diff --git a/apps/api-extractor/src/api/ExtractorConfig.ts b/apps/api-extractor/src/api/ExtractorConfig.ts index 1cdf8c96eb2..e55fa31e748 100644 --- a/apps/api-extractor/src/api/ExtractorConfig.ts +++ b/apps/api-extractor/src/api/ExtractorConfig.ts @@ -172,6 +172,25 @@ export interface IExtractorConfigApiReport { fileName: string; } +/** Default {@link IConfigApiReport.reportVariants} */ +const defaultApiReportVariants: readonly ApiReportVariant[] = ['complete']; + +/** + * Default {@link IConfigApiReport.tagsToReport}. + * + * @remarks + * Note that this list is externally documented, and directly affects report output. + * Also note that the order of tags in this list is significant, as it determines the order of tags in the report. + * Any changes to this list should be considered breaking. + */ +const defaultTagsToReport: Readonly> = { + '@sealed': true, + '@virtual': true, + '@override': true, + '@eventProperty': true, + '@deprecated': true +}; + interface IExtractorConfigParameters { projectFolder: string; packageJson: INodePackageJson | undefined; @@ -186,6 +205,7 @@ interface IExtractorConfigParameters { reportFolder: string; reportTempFolder: string; apiReportIncludeForgottenExports: boolean; + tagsToReport: Readonly>; docModelEnabled: boolean; apiJsonFilePath: string; docModelIncludeForgottenExports: boolean; @@ -281,6 +301,8 @@ export class ExtractorConfig { public readonly reportFolder: string; /** {@inheritDoc IConfigApiReport.reportTempFolder} */ public readonly reportTempFolder: string; + /** {@inheritDoc IConfigApiReport.tagsToReport} */ + public readonly tagsToReport: Readonly>; /** * Gets the file path for the "complete" (default) report configuration, if one was specified. @@ -371,6 +393,7 @@ export class ExtractorConfig { this.reportConfigs = parameters.reportConfigs; this.reportFolder = parameters.reportFolder; this.reportTempFolder = parameters.reportTempFolder; + this.tagsToReport = parameters.tagsToReport; this.docModelEnabled = parameters.docModelEnabled; this.apiJsonFilePath = parameters.apiJsonFilePath; this.docModelIncludeForgottenExports = parameters.docModelIncludeForgottenExports; @@ -915,6 +938,7 @@ export class ExtractorConfig { let reportFolder: string = tokenContext.projectFolder; let reportTempFolder: string = tokenContext.projectFolder; const reportConfigs: IExtractorConfigApiReport[] = []; + let tagsToReport: Record<`@${string}`, boolean> = {}; if (apiReportEnabled) { // Undefined case checked above where we assign `apiReportEnabled` const apiReportConfig: IConfigApiReport = configObject.apiReport!; @@ -947,7 +971,8 @@ export class ExtractorConfig { reportFileNameBase = ''; } - const reportVariantKinds: ApiReportVariant[] = apiReportConfig.reportVariants ?? ['complete']; + const reportVariantKinds: readonly ApiReportVariant[] = + apiReportConfig.reportVariants ?? defaultApiReportVariants; for (const reportVariantKind of reportVariantKinds) { // Omit the variant kind from the "complete" report file name for simplicity and for backwards compatibility. @@ -981,6 +1006,11 @@ export class ExtractorConfig { tokenContext ); } + + tagsToReport = { + ...defaultTagsToReport, + ...apiReportConfig.tagsToReport + }; } let docModelEnabled: boolean = false; @@ -1101,6 +1131,7 @@ export class ExtractorConfig { reportFolder, reportTempFolder, apiReportIncludeForgottenExports, + tagsToReport, docModelEnabled, apiJsonFilePath, docModelIncludeForgottenExports, diff --git a/apps/api-extractor/src/api/IConfigFile.ts b/apps/api-extractor/src/api/IConfigFile.ts index 1a0884d1af0..efad12c4aa5 100644 --- a/apps/api-extractor/src/api/IConfigFile.ts +++ b/apps/api-extractor/src/api/IConfigFile.ts @@ -139,6 +139,22 @@ export interface IConfigApiReport { * @defaultValue `false` */ includeForgottenExports?: boolean; + + /** + * Specifies a list of {@link https://tsdoc.org/ | TSDoc} tags that should be reported in the API report file for + * items whose documentation contains them. + * + * @remarks + * Tag names must begin with `@`. + * + * This list may include standard TSDoc tags as well as custom ones. + * TODO: document requirements around custom tags. + * + * Note that an item's release tag will always reported; this behavior cannot be overridden. + * + * @defaultValue `@sealed`, `@virtual`, `@override`, `@eventProperty`, `@deprecated` + */ + tagsToReport?: Readonly>; } /** diff --git a/apps/api-extractor/src/generators/ApiReportGenerator.ts b/apps/api-extractor/src/generators/ApiReportGenerator.ts index 0a65c999507..46a12be8ba1 100644 --- a/apps/api-extractor/src/generators/ApiReportGenerator.ts +++ b/apps/api-extractor/src/generators/ApiReportGenerator.ts @@ -558,34 +558,57 @@ export class ApiReportGenerator { if (!collector.isAncillaryDeclaration(astDeclaration)) { const footerParts: string[] = []; const apiItemMetadata: ApiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); + + // 1. Release tag (if present) if (!apiItemMetadata.releaseTagSameAsParent) { if (apiItemMetadata.effectiveReleaseTag !== ReleaseTag.None) { footerParts.push(ReleaseTag.getTagName(apiItemMetadata.effectiveReleaseTag)); } } - if (apiItemMetadata.isSealed) { + // 2. Enumerate configured tags, reporting standard system tags first and then other configured tags. + // Note that the ordering we handle the standard tags is important for backwards compatibility. + // Also note that we had special mechanisms for checking whether or not an item is documented with these tags, + // so they are checked specially. + const { + '@sealed': reportSealedTag, + '@virtual': reportVirtualTag, + '@override': reportOverrideTag, + '@eventProperty': reportEventPropertyTag, + '@deprecated': reportDeprecatedTag, + ...otherTagsToReport + } = collector.extractorConfig.tagsToReport; + + // 2.a Check for standard tags and report those that are both configured and present in the metadata. + if (reportSealedTag && apiItemMetadata.isSealed) { footerParts.push('@sealed'); } - - if (apiItemMetadata.isVirtual) { + if (reportVirtualTag && apiItemMetadata.isVirtual) { footerParts.push('@virtual'); } - - if (apiItemMetadata.isOverride) { + if (reportOverrideTag && apiItemMetadata.isOverride) { footerParts.push('@override'); } - - if (apiItemMetadata.isEventProperty) { + if (reportEventPropertyTag && apiItemMetadata.isEventProperty) { footerParts.push('@eventProperty'); } + if (reportDeprecatedTag && apiItemMetadata.tsdocComment?.deprecatedBlock) { + footerParts.push('@deprecated'); + } - if (apiItemMetadata.tsdocComment) { - if (apiItemMetadata.tsdocComment.deprecatedBlock) { - footerParts.push('@deprecated'); + // 2.b Check for other configured tags and report those that are present in the tsdoc metadata. + for (const [tag, reportTag] of Object.entries(otherTagsToReport)) { + if (reportTag) { + // If the tag was not handled specially, check if it is present in the metadata. + if (apiItemMetadata.tsdocComment?.customBlocks.some((block) => block.blockTag.tagName === tag)) { + footerParts.push(tag); + } else if (apiItemMetadata.tsdocComment?.modifierTagSet.hasTagName(tag)) { + footerParts.push(tag); + } } } + // 3. If the item is undocumented, append notice at the end of the list if (apiItemMetadata.undocumented) { footerParts.push('(undocumented)'); diff --git a/apps/api-extractor/src/schemas/api-extractor-defaults.json b/apps/api-extractor/src/schemas/api-extractor-defaults.json index 0a5c813a217..bb6b6157aaf 100644 --- a/apps/api-extractor/src/schemas/api-extractor-defaults.json +++ b/apps/api-extractor/src/schemas/api-extractor-defaults.json @@ -30,7 +30,6 @@ "dtsRollup": { // ("enabled" is required) - "untrimmedFilePath": "/dist/.d.ts", "alphaTrimmedFilePath": "", "betaTrimmedFilePath": "", diff --git a/apps/api-extractor/src/schemas/api-extractor.schema.json b/apps/api-extractor/src/schemas/api-extractor.schema.json index a8ce1970267..fb6817cf6c9 100644 --- a/apps/api-extractor/src/schemas/api-extractor.schema.json +++ b/apps/api-extractor/src/schemas/api-extractor.schema.json @@ -106,6 +106,17 @@ "includeForgottenExports": { "description": "Whether \"forgotten exports\" should be included in the API report file. Forgotten exports are declarations flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to learn more.", "type": "boolean" + }, + + "tagsToReport": { + "description": "Specifies a list of TSDoc tags that should be reported in the API report file for items whose documentation contains them. This can be used to include standard TSDoc tags or custom ones. Specified tag names must begin with \"@\". By default, the following tags are reported: [@sealed, @virtual, @override, @eventProperty, @deprecated]. Tags will appear in the order they are specified in this list. Note that an item's release tag will always reported; this behavior cannot be overridden.", + "type": "object", + "patternProperties": { + "^@[^\\s]*$": { + "type": "boolean" + } + }, + "additionalProperties": false } }, "required": ["enabled"], diff --git a/build-tests/api-documenter-test/config/api-extractor.json b/build-tests/api-documenter-test/config/api-extractor.json index 17b82fa03cc..3e2857a5472 100644 --- a/build-tests/api-documenter-test/config/api-extractor.json +++ b/build-tests/api-documenter-test/config/api-extractor.json @@ -1,12 +1,15 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "$schema": "../../../apps/api-extractor/src/schemas/api-extractor.schema.json", "mainEntryPointFilePath": "/lib/index.d.ts", "newlineKind": "crlf", "apiReport": { - "enabled": true + "enabled": true, + "tagsToReport": { + "@myCustomTag": true + } }, "docModel": { diff --git a/build-tests/api-documenter-test/etc/api-documenter-test.api.md b/build-tests/api-documenter-test/etc/api-documenter-test.api.md index 18bab020adb..9ed61a12c24 100644 --- a/build-tests/api-documenter-test/etc/api-documenter-test.api.md +++ b/build-tests/api-documenter-test/etc/api-documenter-test.api.md @@ -184,7 +184,7 @@ export namespace OuterNamespace { let nestedVariable: boolean; } -// @public +// @public @myCustomTag export class SystemEvent { addHandler(handler: () => void): void; } diff --git a/build-tests/api-extractor-lib3-test/config/api-extractor.json b/build-tests/api-extractor-lib3-test/config/api-extractor.json index 609b62dc032..76705d69b97 100644 --- a/build-tests/api-extractor-lib3-test/config/api-extractor.json +++ b/build-tests/api-extractor-lib3-test/config/api-extractor.json @@ -1,10 +1,17 @@ { - "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "$schema": "../../../apps/api-extractor/src/schemas/api-extractor.schema.json", "mainEntryPointFilePath": "/lib/index.d.ts", "apiReport": { - "enabled": true + "enabled": true, + "tagsToReport": { + // Disable reporting of `@virtual`, which is reported by default + "@virtual": false, + // Enable reporting of our custom TSDoc tags. + "@customBlockTag": true, + "@customModifierTag": true + } }, "docModel": { diff --git a/build-tests/api-extractor-lib3-test/dist/api-extractor-lib3-test.d.ts b/build-tests/api-extractor-lib3-test/dist/api-extractor-lib3-test.d.ts index 433e071b772..a65b3887b9c 100644 --- a/build-tests/api-extractor-lib3-test/dist/api-extractor-lib3-test.d.ts +++ b/build-tests/api-extractor-lib3-test/dist/api-extractor-lib3-test.d.ts @@ -11,8 +11,16 @@ import { Lib1Class } from 'api-extractor-lib1-test'; export { Lib1Class } -/** @public */ +/** + * @customModifierTag + * @public + */ export declare class Lib3Class { + /** + * I am a documented property! + * @customBlockTag My docs include a custom block tag! + * @virtual @override + */ prop: boolean; } diff --git a/build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md b/build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md index f79328f79fe..5c538497301 100644 --- a/build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md +++ b/build-tests/api-extractor-lib3-test/etc/api-extractor-lib3-test.api.md @@ -8,9 +8,9 @@ import { Lib1Class } from 'api-extractor-lib1-test'; export { Lib1Class } -// @public (undocumented) +// @public @customModifierTag (undocumented) export class Lib3Class { - // (undocumented) + // @override @customBlockTag prop: boolean; } diff --git a/build-tests/api-extractor-lib3-test/src/index.ts b/build-tests/api-extractor-lib3-test/src/index.ts index 51aa8d2c85f..577ebd8f710 100644 --- a/build-tests/api-extractor-lib3-test/src/index.ts +++ b/build-tests/api-extractor-lib3-test/src/index.ts @@ -12,7 +12,15 @@ export { Lib1Class } from 'api-extractor-lib1-test'; -/** @public */ +/** + * @customModifierTag + * @public + */ export class Lib3Class { + /** + * I am a documented property! + * @customBlockTag My docs include a custom block tag! + * @virtual @override + */ prop: boolean; } diff --git a/build-tests/api-extractor-lib3-test/tsdoc.json b/build-tests/api-extractor-lib3-test/tsdoc.json new file mode 100644 index 00000000000..e47e6c98b14 --- /dev/null +++ b/build-tests/api-extractor-lib3-test/tsdoc.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + + "tagDefinitions": [ + { + "tagName": "@customModifierTag", + "syntaxKind": "modifier" + }, + { + "tagName": "@customBlockTag", + "syntaxKind": "modifier" + } + ], + "supportForTags": { + "@customModifierTag": true, + "@customBlockTag": true + } +} diff --git a/common/reviews/api/api-extractor.api.md b/common/reviews/api/api-extractor.api.md index 7b49ce35648..0fa12fadabe 100644 --- a/common/reviews/api/api-extractor.api.md +++ b/common/reviews/api/api-extractor.api.md @@ -87,6 +87,7 @@ export class ExtractorConfig { readonly reportTempFolder: string; readonly rollupEnabled: boolean; readonly skipLibCheck: boolean; + readonly tagsToReport: Readonly>; readonly testMode: boolean; static tryLoadForFolder(options: IExtractorConfigLoadForFolderOptions): IExtractorConfigPrepareOptions | undefined; readonly tsconfigFilePath: string; @@ -186,6 +187,7 @@ export interface IConfigApiReport { reportFolder?: string; reportTempFolder?: string; reportVariants?: ApiReportVariant[]; + tagsToReport?: Readonly>; } // @public