From efd190ba13de1728686b5893c9ef9fb98c24ea0e Mon Sep 17 00:00:00 2001 From: Yacine Hmito Date: Sun, 30 Dec 2018 14:24:52 +0100 Subject: [PATCH] New way to resolve & generate TSDoc metadata file The resolver of the TSDoc metadata file now follows the following proposal: https://github.com/Microsoft/tsdoc/issues/7#issuecomment-450535066 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. --- package.json | 8 +- src/analyzer/PackageMetadataManager.ts | 72 +++++++-- .../test/PackageMetadataManager.test.ts | 138 ++++++++++++++++++ .../package-default/package.json | 4 + .../package-inferred-from-main/package.json | 5 + .../package.json | 7 + .../package.json | 6 + src/api/Extractor.ts | 19 ++- src/api/IExtractorConfig.ts | 26 ++++ src/index.ts | 1 + src/schemas/api-extractor-defaults.json | 5 + src/schemas/api-extractor.schema.json | 17 +++ tsconfig.json | 3 +- 13 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 src/analyzer/test/PackageMetadataManager.test.ts create mode 100644 src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json create mode 100644 src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json create mode 100644 src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json create mode 100644 src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json diff --git a/package.json b/package.json index 6853be20f29..35d8b8d7a1e 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/analyzer/PackageMetadataManager.ts b/src/analyzer/PackageMetadataManager.ts index 14ea88d0cb3..9fccf20fd59 100644 --- a/src/analyzer/PackageMetadataManager.ts +++ b/src/analyzer/PackageMetadataManager.ts @@ -61,26 +61,70 @@ export class PackageMetadataManager { private readonly _packageMetadataByPackageJsonPath: Map = new Map(); - 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, @@ -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); diff --git a/src/analyzer/test/PackageMetadataManager.test.ts b/src/analyzer/test/PackageMetadataManager.test.ts new file mode 100644 index 00000000000..1d784979c37 --- /dev/null +++ b/src/analyzer/test/PackageMetadataManager.test.ts @@ -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]; +} diff --git a/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json new file mode 100644 index 00000000000..1a58930cee9 --- /dev/null +++ b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-default/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-default", + "version": "1.0.0" +} diff --git a/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json new file mode 100644 index 00000000000..30a7f604a96 --- /dev/null +++ b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-main/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-inferred-from-main", + "version": "1.0.0", + "main": "path/to/main.js" +} diff --git a/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json new file mode 100644 index 00000000000..fbb048f47ef --- /dev/null +++ b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-tsdoc-metadata/package.json @@ -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" +} diff --git a/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json new file mode 100644 index 00000000000..bb73979d054 --- /dev/null +++ b/src/analyzer/test/test-data/tsdoc-metadata-path-inference/package-inferred-from-typings/package.json @@ -0,0 +1,6 @@ +{ + "name": "package-inferred-from-typings", + "version": "1.0.0", + "main": "path/to/main.js", + "typings": "path/to/typings.d.ts" +} diff --git a/src/api/Extractor.ts b/src/api/Extractor.ts index 2ced10321f1..93327f4ad60 100644 --- a/src/api/Extractor.ts +++ b/src/api/Extractor.ts @@ -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; @@ -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'); } @@ -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) @@ -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 diff --git a/src/api/IExtractorConfig.ts b/src/api/IExtractorConfig.ts index d443aabe392..84dbf7ea046 100644 --- a/src/api/IExtractorConfig.ts +++ b/src/api/IExtractorConfig.ts @@ -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. @@ -297,4 +317,10 @@ export interface IExtractorConfig { * @beta */ dtsRollup?: IExtractorDtsRollupConfig; + + /** + * {@inheritdoc IExtractorTsdocMetadataConfig} + * @beta + */ + tsdocMetadata?: IExtractorTsdocMetadataConfig; } diff --git a/src/index.ts b/src/index.ts index 546422cbae8..237ee1099ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ export { IExtractorApiReviewFileConfig, IExtractorApiJsonFileConfig, IExtractorDtsRollupConfig, + IExtractorTsdocMetadataConfig, IExtractorConfig } from './api/IExtractorConfig'; diff --git a/src/schemas/api-extractor-defaults.json b/src/schemas/api-extractor-defaults.json index 3d685d11935..5a7912835c3 100644 --- a/src/schemas/api-extractor-defaults.json +++ b/src/schemas/api-extractor-defaults.json @@ -35,5 +35,10 @@ "publishFolderForPublic": "./dist/public", "mainDtsRollupPath": "" + }, + + "tsdocMetadata": { + "enabled": true, + "tsdocMetadataPath": "" } } diff --git a/src/schemas/api-extractor.schema.json b/src/schemas/api-extractor.schema.json index 04a5d5e27b3..ec7674b3dae 100644 --- a/src/schemas/api-extractor.schema.json +++ b/src/schemas/api-extractor.schema.json @@ -164,6 +164,23 @@ }, "required": [ "enabled" ], "additionalProperties": false + }, + + "tsdocMetadata": { + "description": "Configures how the TSDoc metadata file will be generated", + "type": "object", + "properties": { + "enabled": { + "description": "Whether to generate the TSDoc metadata file. The default is true.", + "type": "boolean" + }, + "tsdocMetadataPath": { + "description": "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\".", + "type": "string" + }, + }, + "required": [ "enabled" ], + "additionalProperties": false } }, "required": [ "compiler", "project" ], diff --git a/tsconfig.json b/tsconfig.json index ceba71830a1..8943980c329 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "types": [ - "jest" + "jest", + "node" ] } }