Skip to content

Commit

Permalink
New way to resolve & generate TSDoc metadata file
Browse files Browse the repository at this point in the history
The resolver of the TSDoc metadata file now follows the following proposal:
microsoft/tsdoc#7 (comment)
The default location of the generated TSDoc metadata file is inferred as to match the resolver.
This behaviour can be overriden in api-extractor.json.
Generation of the TSDoc medatadata file can even be disabled.
  • Loading branch information
yacinehmito committed Jan 15, 2019
1 parent 6e7a07b commit 7280c73
Show file tree
Hide file tree
Showing 18 changed files with 307 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
"temp": true,
"coverage": true
}
}
}
8 changes: 5 additions & 3 deletions apps/api-extractor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
"resolve": "1.8.1"
},
"devDependencies": {
"@microsoft/node-library-build": "6.0.15",
"@microsoft/rush-stack-compiler-3.0": "0.1.0",
"tslint-microsoft-contrib": "~5.2.1",
"@types/jest": "23.3.11",
"@types/lodash": "4.14.116",
"@types/node": "8.5.8",
"gulp": "~3.9.1",
"@microsoft/node-library-build": "6.0.15",
"@types/jest": "23.3.11"
"jest": "~23.6.0",
"tslint-microsoft-contrib": "~5.2.1"
}
}
72 changes: 58 additions & 14 deletions apps/api-extractor/src/analyzer/PackageMetadataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,70 @@ export class PackageMetadataManager {
private readonly _packageMetadataByPackageJsonPath: Map<string, PackageMetadata>
= new Map<string, PackageMetadata>();

public static writeTsdocMetadataFile(packageJsonFolder: string): void {
// This feature is still being standardized: https://github.com/Microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
const tsdocMetadataPath: string = path.join(packageJsonFolder,
'dist', PackageMetadataManager.tsdocMetadataFilename);
// This feature is still being standardized: https://github.com/Microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
private static _resolveTsdocMetadataPathFromPackageJson(packageFolder: string, packageJson: IPackageJson): string {
const { tsdocMetadataFilename } = PackageMetadataManager;
let tsdocMetadataRelativePath: string;
if (packageJson.tsdocMetadata) {
tsdocMetadataRelativePath = packageJson.tsdocMetadata;
} else if (packageJson.typings) {
tsdocMetadataRelativePath = path.join(
path.dirname(packageJson.typings),
tsdocMetadataFilename
);
} else if (packageJson.main) {
tsdocMetadataRelativePath = path.join(
path.dirname(packageJson.main),
tsdocMetadataFilename
);
} else {
tsdocMetadataRelativePath = tsdocMetadataFilename;
}
const tsdocMetadataPath: string = path.resolve(
packageFolder,
tsdocMetadataRelativePath
);
return tsdocMetadataPath;
}

/**
* @param tsdocMetadataPath - An explicit path that can be configured in api-extractor.json.
* If this parameter is not an empty string, it overrides the normal path calculation.
* @returns the absolute path to the TSDoc metadata file
*/
public static resolveTsdocMetadataPath(
packageFolder: string,
packageJson: IPackageJson,
tsdocMetadataPath?: string
): string {
if (tsdocMetadataPath) {
return path.resolve(packageFolder, tsdocMetadataPath);
}
return PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(
packageFolder,
packageJson
);
}

/**
* Writes the TSDoc metadata file to the specified output file.
*/
public static writeTsdocMetadataFile(tsdocMetadataPath: string): void {
const fileObject: Object = {
tsdocVersion: '0.12',
toolPackages: [
{
packageName: '@microsoft/api-extractor',
packageVersion: Extractor.version
packageName: '@microsoft/api-extractor',
packageVersion: Extractor.version
}
]
};

const fileContent: string =
'// This file is read by tools that parse documentation comments conforming to the TSDoc standard.\n'
+ '// It should be published with your NPM package. It should not be tracked by Git.\n'
+ JsonFile.stringify(fileObject);
'// This file is read by tools that parse documentation comments conforming to the TSDoc standard.\n' +
'// It should be published with your NPM package. It should not be tracked by Git.\n' +
JsonFile.stringify(fileObject);

FileSystem.writeFile(tsdocMetadataPath, fileContent, {
convertLineEndings: NewlineKind.CrLf,
Expand Down Expand Up @@ -112,12 +156,12 @@ export class PackageMetadataManager {

const packageJsonFolder: string = path.dirname(packageJsonFilePath);

// This feature is still being standardized: https://github.com/Microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
let aedocSupported: boolean = false;

const tsdocMetadataPath: string = path.join(packageJsonFolder,
'dist', PackageMetadataManager.tsdocMetadataFilename);
const tsdocMetadataPath: string = PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(
packageJsonFolder,
packageJson
);

if (FileSystem.exists(tsdocMetadataPath)) {
this._logger.logVerbose('Found metadata in ' + tsdocMetadataPath);
Expand Down
138 changes: 138 additions & 0 deletions apps/api-extractor/src/analyzer/test/PackageMetadataManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@

import * as path from 'path';
import { PackageMetadataManager } from '../PackageMetadataManager';
import { FileSystem, PackageJsonLookup, IPackageJson } from '@microsoft/node-core-library';

/* tslint:disable:typedef */

describe('PackageMetadataManager', () => {
describe('.writeTsdocMetadataFile()', () => {
const originalWriteFile = FileSystem.writeFile;
const mockWriteFile: jest.Mock = jest.fn();
beforeAll(() => {
FileSystem.writeFile = mockWriteFile;
});
afterEach(() => {
mockWriteFile.mockClear();
});
afterAll(() => {
FileSystem.writeFile = originalWriteFile;
});

it('writes the tsdoc metadata file at the provided path', () => {
PackageMetadataManager.writeTsdocMetadataFile('/foo/bar');
expect(firstArgument(mockWriteFile)).toBe('/foo/bar');
});
});

describe('.resolveTsdocMetadataPath()', () => {
describe('when an empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath: string = '';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata path as given by "tsdocMetadata" relative to the folder of package.json', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-tsdoc-metadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, packageJson.tsdocMetadata));
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "typings"', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, path.dirname(packageJson.typings!), 'tsdoc-metadata.json'));
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the same folder as the path of "main"', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, path.dirname(packageJson.main!), 'tsdoc-metadata.json'));
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file "tsdoc-metadata.json" in the folder where package.json is located', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, 'tsdoc-metadata.json'));
});
});
});
describe('when a non-empty tsdocMetadataPath is provided', () => {
const tsdocMetadataPath: string = 'path/to/custom-tsdoc-metadata.json';
describe('given a package.json where the field "tsdocMetadata" is defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-tsdocMetadata');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, tsdocMetadataPath));
});
});
describe('given a package.json where the field "typings" is defined and "tsdocMetadata" is not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-typings');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, tsdocMetadataPath));
});
});
describe('given a package.json where the field "main" is defined but not "typings" nor "tsdocMetadata"', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-inferred-from-main');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, tsdocMetadataPath));
});
});
describe('given a package.json where the fields "main", "typings" and "tsdocMetadata" are not defined', () => {
it('outputs the tsdoc metadata file at the provided path in the folder where package.json is located', () => {
const {
packageFolder,
packageJson
} = getPackageMetadata('package-default');
expect(PackageMetadataManager.resolveTsdocMetadataPath(packageFolder, packageJson, tsdocMetadataPath))
.toBe(path.resolve(packageFolder, tsdocMetadataPath));
});
});
});
});
});

/* tslint:enable:typedef */

const packageJsonLookup: PackageJsonLookup = new PackageJsonLookup();

function resolveInTestPackage(testPackageName: string, ...args: string[]): string {
return path.resolve(__dirname, 'test-data/tsdoc-metadata-path-inference', testPackageName, ...args);
}

function getPackageMetadata(testPackageName: string): { packageFolder: string, packageJson: IPackageJson } {
const packageFolder: string = resolveInTestPackage(testPackageName);
const packageJson: IPackageJson | undefined = packageJsonLookup.tryLoadPackageJsonFor(packageFolder);
if (!packageJson) {
throw new Error('There should be a package.json file in the test package');
}
return { packageFolder, packageJson };
}

// tslint:disable-next-line:no-any
function firstArgument(mockFn: jest.Mock): any {
return mockFn.mock.calls[0][0];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "package-default",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "package-inferred-from-main",
"version": "1.0.0",
"main": "path/to/main.js"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "package-inferred-from-tsdoc-metadata",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts",
"tsdocMetadata": "path/to/tsdoc-metadata.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "package-inferred-from-typings",
"version": "1.0.0",
"main": "path/to/main.js",
"typings": "path/to/typings.d.ts"
}
19 changes: 14 additions & 5 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export class Extractor {

private static _applyConfigDefaults(config: IExtractorConfig): IExtractorConfig {
// Use the provided config to override the defaults
const normalized: IExtractorConfig = lodash.merge(
const normalized: IExtractorConfig = lodash.merge(
lodash.cloneDeep(Extractor._defaultConfig), config);

return normalized;
Expand Down Expand Up @@ -385,7 +385,8 @@ export class Extractor {
// This helps strict-null-checks to understand that _applyConfigDefaults() eliminated
// any undefined members
if (!(this.actualConfig.policies && this.actualConfig.validationRules
&& this.actualConfig.apiJsonFile && this.actualConfig.apiReviewFile && this.actualConfig.dtsRollup)) {
&& this.actualConfig.apiJsonFile && this.actualConfig.apiReviewFile
&& this.actualConfig.dtsRollup && this.actualConfig.tsdocMetadata)) {
throw new Error('The configuration object wasn\'t normalized properly');
}

Expand Down Expand Up @@ -475,8 +476,16 @@ export class Extractor {

this._generateRollupDtsFiles(collector);

// Write the tsdoc-metadata.json file for this project
PackageMetadataManager.writeTsdocMetadataFile(collector.package.packageFolder);
if (this.actualConfig.tsdocMetadata.enabled) {
// Write the tsdoc-metadata.json file for this project
PackageMetadataManager.writeTsdocMetadataFile(
PackageMetadataManager.resolveTsdocMetadataPath(
collector.package.packageFolder,
collector.package.packageJson,
this.actualConfig.tsdocMetadata.tsdocMetadataPath
)
);
}

if (this._localBuild) {
// For a local build, fail if there were errors (but ignore warnings)
Expand Down Expand Up @@ -590,7 +599,7 @@ export class Extractor {
const compilerLibFolder: string = path.join(options.typescriptCompilerFolder, 'lib');

let foundBaseLib: boolean = false;
const filesToAdd: string[] = [];
const filesToAdd: string[] = [];
for (const libFilename of commandLine.options.lib || []) {
if (libFilename === DEFAULT_BUILTIN_LIBRARY) {
// Ignore the default lib - it'll get added later
Expand Down
26 changes: 26 additions & 0 deletions apps/api-extractor/src/api/IExtractorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,26 @@ export interface IExtractorDtsRollupConfig {
mainDtsRollupPath?: string;
}

/**
* Configures how the tsdoc metadata file will be generated.
*
* @beta
*/
export interface IExtractorTsdocMetadataConfig {
/**
* Whether to generate the tsdoc metadata file. The default is false.
*/
enabled: boolean;

/**
* Specifies where the tsdoc metadata file should be written. The default value is
* an empty string, which causes the path to be automatically inferred from the
* "tsdocMetadata", "typings" or "main" fields of the project's package.json.
* If none of these fields are set, it defaults to "tsdoc-metadata.json".
*/
tsdocMetadataPath?: string;
}

/**
* Configuration options for the API Extractor tool. These options can be loaded
* from a JSON config file.
Expand Down Expand Up @@ -297,4 +317,10 @@ export interface IExtractorConfig {
* @beta
*/
dtsRollup?: IExtractorDtsRollupConfig;

/**
* {@inheritdoc IExtractorTsdocMetadataConfig}
* @beta
*/
tsdocMetadata?: IExtractorTsdocMetadataConfig;
}
1 change: 1 addition & 0 deletions apps/api-extractor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
IExtractorApiReviewFileConfig,
IExtractorApiJsonFileConfig,
IExtractorDtsRollupConfig,
IExtractorTsdocMetadataConfig,
IExtractorConfig
} from './api/IExtractorConfig';

Expand Down
5 changes: 5 additions & 0 deletions apps/api-extractor/src/schemas/api-extractor-defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,10 @@
"publishFolderForPublic": "./dist/public",

"mainDtsRollupPath": ""
},

"tsdocMetadata": {
"enabled": true,
"tsdocMetadataPath": ""
}
}
Loading

0 comments on commit 7280c73

Please sign in to comment.