Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-extractor] Customize which TSDoc tags appear in API reports #5079

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
33 changes: 32 additions & 1 deletion apps/api-extractor/src/api/ExtractorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<`@${string}`, boolean>> = {
'@sealed': true,
'@virtual': true,
'@override': true,
'@eventProperty': true,
'@deprecated': true
};

interface IExtractorConfigParameters {
projectFolder: string;
packageJson: INodePackageJson | undefined;
Expand All @@ -186,6 +205,7 @@ interface IExtractorConfigParameters {
reportFolder: string;
reportTempFolder: string;
apiReportIncludeForgottenExports: boolean;
tagsToReport: Readonly<Record<`@${string}`, boolean>>;
docModelEnabled: boolean;
apiJsonFilePath: string;
docModelIncludeForgottenExports: boolean;
Expand Down Expand Up @@ -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<Record<`@${string}`, boolean>>;

/**
* Gets the file path for the "complete" (default) report configuration, if one was specified.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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!;
Expand Down Expand Up @@ -947,7 +971,8 @@ export class ExtractorConfig {
reportFileNameBase = '<unscopedPackageName>';
}

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.
Expand Down Expand Up @@ -981,6 +1006,11 @@ export class ExtractorConfig {
tokenContext
);
}

tagsToReport = {
...defaultTagsToReport,
...apiReportConfig.tagsToReport
};
}

let docModelEnabled: boolean = false;
Expand Down Expand Up @@ -1101,6 +1131,7 @@ export class ExtractorConfig {
reportFolder,
reportTempFolder,
apiReportIncludeForgottenExports,
tagsToReport,
docModelEnabled,
apiJsonFilePath,
docModelIncludeForgottenExports,
Expand Down
16 changes: 16 additions & 0 deletions apps/api-extractor/src/api/IConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add examples

Josmithr marked this conversation as resolved.
Show resolved Hide resolved
*/
tagsToReport?: Readonly<Record<`@${string}`, boolean>>;
}

/**
Expand Down
43 changes: 33 additions & 10 deletions apps/api-extractor/src/generators/ApiReportGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

"dtsRollup": {
// ("enabled" is required)

"untrimmedFilePath": "<projectFolder>/dist/<unscopedPackageName>.d.ts",
"alphaTrimmedFilePath": "",
"betaTrimmedFilePath": "",
Expand Down
11 changes: 11 additions & 0 deletions apps/api-extractor/src/schemas/api-extractor.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
7 changes: 5 additions & 2 deletions build-tests/api-documenter-test/config/api-extractor.json
Original file line number Diff line number Diff line change
@@ -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": "<projectFolder>/lib/index.d.ts",

"newlineKind": "crlf",

"apiReport": {
"enabled": true
"enabled": true,
"tagsToReport": {
"@myCustomTag": true
}
},

"docModel": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export namespace OuterNamespace {
let nestedVariable: boolean;
}

// @public
// @public @myCustomTag
export class SystemEvent {
addHandler(handler: () => void): void;
}
Expand Down
11 changes: 9 additions & 2 deletions build-tests/api-extractor-lib3-test/config/api-extractor.json
Original file line number Diff line number Diff line change
@@ -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": "<projectFolder>/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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
10 changes: 9 additions & 1 deletion build-tests/api-extractor-lib3-test/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
20 changes: 20 additions & 0 deletions build-tests/api-extractor-lib3-test/tsdoc.json
Original file line number Diff line number Diff line change
@@ -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
}
}
2 changes: 2 additions & 0 deletions common/reviews/api/api-extractor.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export class ExtractorConfig {
readonly reportTempFolder: string;
readonly rollupEnabled: boolean;
readonly skipLibCheck: boolean;
readonly tagsToReport: Readonly<Record<`@${string}`, boolean>>;
readonly testMode: boolean;
static tryLoadForFolder(options: IExtractorConfigLoadForFolderOptions): IExtractorConfigPrepareOptions | undefined;
readonly tsconfigFilePath: string;
Expand Down Expand Up @@ -186,6 +187,7 @@ export interface IConfigApiReport {
reportFolder?: string;
reportTempFolder?: string;
reportVariants?: ApiReportVariant[];
tagsToReport?: Readonly<Record<`@${string}`, boolean>>;
}

// @public
Expand Down
Loading