Skip to content

Commit 875d3de

Browse files
Josmithrexample
andauthored
[api-extractor] Customize which TSDoc tags appear in API reports (#5079)
* feat: [WIP]: Allow customization of tags included in API reports * docs: Update code comments * fix: Align schema property name with code * test: Update test case to leverage new functionality * refactor: Rename property * refactor: Make the structure of `tagsToReport` easier to override * fix: Config handling * test: Update test scenario * docs: Fix comment * docs: Update comments * docs: Examples * test: Add unit test case * revert: Test * test: Add and fix tests * revert: Line formatting change * docs: Add changesets * revert: Schema base changes * build: Add missing test dependency * build: Update lockfile * test: Update test to work `api-extractor-scenarios` limitations with regards to `tsdoc.json`s * test: Fix test case --------- Co-authored-by: Joshua Smithrud <[email protected]> Co-authored-by: Joshua Smithrud <[email protected]>
1 parent 6b358ea commit 875d3de

29 files changed

+326
-27
lines changed

apps/api-extractor/src/api/ExtractorConfig.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
} from '@rushstack/node-core-library';
1919
import { type IRigConfig, RigConfig } from '@rushstack/rig-package';
2020
import { EnumMemberOrder, ReleaseTag } from '@microsoft/api-extractor-model';
21-
import { TSDocConfiguration } from '@microsoft/tsdoc';
21+
import { TSDocConfiguration, TSDocTagDefinition } from '@microsoft/tsdoc';
2222
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
2323

2424
import type {
@@ -173,6 +173,25 @@ export interface IExtractorConfigApiReport {
173173
fileName: string;
174174
}
175175

176+
/** Default {@link IConfigApiReport.reportVariants} */
177+
const defaultApiReportVariants: readonly ApiReportVariant[] = ['complete'];
178+
179+
/**
180+
* Default {@link IConfigApiReport.tagsToReport}.
181+
*
182+
* @remarks
183+
* Note that this list is externally documented, and directly affects report output.
184+
* Also note that the order of tags in this list is significant, as it determines the order of tags in the report.
185+
* Any changes to this list should be considered breaking.
186+
*/
187+
const defaultTagsToReport: Readonly<Record<`@${string}`, boolean>> = {
188+
'@sealed': true,
189+
'@virtual': true,
190+
'@override': true,
191+
'@eventProperty': true,
192+
'@deprecated': true
193+
};
194+
176195
interface IExtractorConfigParameters {
177196
projectFolder: string;
178197
packageJson: INodePackageJson | undefined;
@@ -187,6 +206,7 @@ interface IExtractorConfigParameters {
187206
reportFolder: string;
188207
reportTempFolder: string;
189208
apiReportIncludeForgottenExports: boolean;
209+
tagsToReport: Readonly<Record<`@${string}`, boolean>>;
190210
docModelGenerationOptions: IApiModelGenerationOptions | undefined;
191211
apiJsonFilePath: string;
192212
docModelIncludeForgottenExports: boolean;
@@ -282,6 +302,8 @@ export class ExtractorConfig {
282302
public readonly reportFolder: string;
283303
/** {@inheritDoc IConfigApiReport.reportTempFolder} */
284304
public readonly reportTempFolder: string;
305+
/** {@inheritDoc IConfigApiReport.tagsToReport} */
306+
public readonly tagsToReport: Readonly<Record<`@${string}`, boolean>>;
285307

286308
/**
287309
* Gets the file path for the "complete" (default) report configuration, if one was specified.
@@ -375,6 +397,7 @@ export class ExtractorConfig {
375397
reportConfigs,
376398
reportFolder,
377399
reportTempFolder,
400+
tagsToReport,
378401
docModelGenerationOptions,
379402
apiJsonFilePath,
380403
docModelIncludeForgottenExports,
@@ -407,6 +430,7 @@ export class ExtractorConfig {
407430
this.reportConfigs = reportConfigs;
408431
this.reportFolder = reportFolder;
409432
this.reportTempFolder = reportTempFolder;
433+
this.tagsToReport = tagsToReport;
410434
this.docModelGenerationOptions = docModelGenerationOptions;
411435
this.apiJsonFilePath = apiJsonFilePath;
412436
this.docModelIncludeForgottenExports = docModelIncludeForgottenExports;
@@ -960,12 +984,17 @@ export class ExtractorConfig {
960984
}
961985
}
962986

987+
if (configObject.apiReport?.tagsToReport) {
988+
_validateTagsToReport(configObject.apiReport.tagsToReport);
989+
}
990+
963991
const apiReportEnabled: boolean = configObject.apiReport?.enabled ?? false;
964992
const apiReportIncludeForgottenExports: boolean =
965993
configObject.apiReport?.includeForgottenExports ?? false;
966994
let reportFolder: string = tokenContext.projectFolder;
967995
let reportTempFolder: string = tokenContext.projectFolder;
968996
const reportConfigs: IExtractorConfigApiReport[] = [];
997+
let tagsToReport: Record<`@${string}`, boolean> = {};
969998
if (apiReportEnabled) {
970999
// Undefined case checked above where we assign `apiReportEnabled`
9711000
const apiReportConfig: IConfigApiReport = configObject.apiReport!;
@@ -998,7 +1027,8 @@ export class ExtractorConfig {
9981027
reportFileNameBase = '<unscopedPackageName>';
9991028
}
10001029

1001-
const reportVariantKinds: ApiReportVariant[] = apiReportConfig.reportVariants ?? ['complete'];
1030+
const reportVariantKinds: readonly ApiReportVariant[] =
1031+
apiReportConfig.reportVariants ?? defaultApiReportVariants;
10021032

10031033
for (const reportVariantKind of reportVariantKinds) {
10041034
// Omit the variant kind from the "complete" report file name for simplicity and for backwards compatibility.
@@ -1032,6 +1062,11 @@ export class ExtractorConfig {
10321062
tokenContext
10331063
);
10341064
}
1065+
1066+
tagsToReport = {
1067+
...defaultTagsToReport,
1068+
...apiReportConfig.tagsToReport
1069+
};
10351070
}
10361071

10371072
let docModelGenerationOptions: IApiModelGenerationOptions | undefined = undefined;
@@ -1188,6 +1223,7 @@ export class ExtractorConfig {
11881223
reportFolder,
11891224
reportTempFolder,
11901225
apiReportIncludeForgottenExports,
1226+
tagsToReport,
11911227
docModelGenerationOptions,
11921228
apiJsonFilePath,
11931229
docModelIncludeForgottenExports,
@@ -1319,3 +1355,47 @@ export class ExtractorConfig {
13191355
throw new Error(`The "${fieldName}" value contains extra token characters ("<" or ">"): ${value}`);
13201356
}
13211357
}
1358+
1359+
const releaseTags: Set<string> = new Set(['@public', '@alpha', '@beta', '@internal']);
1360+
1361+
/**
1362+
* Validate {@link ExtractorConfig.tagsToReport}.
1363+
*/
1364+
function _validateTagsToReport(
1365+
tagsToReport: Record<string, boolean>
1366+
): asserts tagsToReport is Record<`@${string}`, boolean> {
1367+
const includedReleaseTags: string[] = [];
1368+
const invalidTags: [string, string][] = []; // tag name, error
1369+
for (const tag of Object.keys(tagsToReport)) {
1370+
if (releaseTags.has(tag)) {
1371+
// If a release tags is specified, regardless of whether it is enabled, we will throw an error.
1372+
// Release tags must not be specified.
1373+
includedReleaseTags.push(tag);
1374+
}
1375+
1376+
// If the tag is invalid, generate an error string from the inner error message.
1377+
try {
1378+
TSDocTagDefinition.validateTSDocTagName(tag);
1379+
} catch (error) {
1380+
invalidTags.push([tag, (error as Error).message]);
1381+
}
1382+
}
1383+
1384+
const errorMessages: string[] = [];
1385+
for (const includedReleaseTag of includedReleaseTags) {
1386+
errorMessages.push(
1387+
`${includedReleaseTag}: Release tags are always included in API reports and must not be specified`
1388+
);
1389+
}
1390+
for (const [invalidTag, innerError] of invalidTags) {
1391+
errorMessages.push(`${invalidTag}: ${innerError}`);
1392+
}
1393+
1394+
if (errorMessages.length > 0) {
1395+
const errorMessage: string = [
1396+
`"tagsToReport" contained one or more invalid tags:`,
1397+
...errorMessages
1398+
].join('\n\t- ');
1399+
throw new Error(errorMessage);
1400+
}
1401+
}

apps/api-extractor/src/api/IConfigFile.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,41 @@ export interface IConfigApiReport {
139139
* @defaultValue `false`
140140
*/
141141
includeForgottenExports?: boolean;
142+
143+
/**
144+
* Specifies a list of {@link https://tsdoc.org/ | TSDoc} tags that should be reported in the API report file for
145+
* items whose documentation contains them.
146+
*
147+
* @remarks
148+
* Tag names must begin with `@`.
149+
*
150+
* This list may include standard TSDoc tags as well as custom ones.
151+
* For more information on defining custom TSDoc tags, see
152+
* {@link https://api-extractor.com/pages/configs/tsdoc_json/#defining-your-own-tsdoc-tags | here}.
153+
*
154+
* Note that an item's release tag will always reported; this behavior cannot be overridden.
155+
*
156+
* @defaultValue `@sealed`, `@virtual`, `@override`, `@eventProperty`, and `@deprecated`
157+
*
158+
* @example Omitting default tags
159+
* To omit the `@sealed` and `@virtual` tags from API reports, you would specify `tagsToReport` as follows:
160+
* ```json
161+
* "tagsToReport": {
162+
* "@sealed": false,
163+
* "@virtual": false
164+
* }
165+
* ```
166+
*
167+
* @example Including additional tags
168+
* To include additional tags to the set included in API reports, you could specify `tagsToReport` like this:
169+
* ```json
170+
* "tagsToReport": {
171+
* "@customTag": true
172+
* }
173+
* ```
174+
* This will result in `@customTag` being included in addition to the default tags.
175+
*/
176+
tagsToReport?: Readonly<Record<`@${string}`, boolean>>;
142177
}
143178

144179
/**

apps/api-extractor/src/api/test/Extractor-custom-tags.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const testDataFolder: string = path.join(__dirname, 'test-data');
1010

1111
describe('Extractor-custom-tags', () => {
1212
describe('should use a TSDocConfiguration', () => {
13-
it.only("with custom TSDoc tags defined in the package's tsdoc.json", () => {
13+
it("with custom TSDoc tags defined in the package's tsdoc.json", () => {
1414
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
1515
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json')
1616
);
@@ -20,7 +20,7 @@ describe('Extractor-custom-tags', () => {
2020
expect(tsdocConfiguration.tryGetTagDefinition('@inline')).not.toBe(undefined);
2121
expect(tsdocConfiguration.tryGetTagDefinition('@modifier')).not.toBe(undefined);
2222
});
23-
it.only("with custom TSDoc tags enabled per the package's tsdoc.json", () => {
23+
it("with custom TSDoc tags enabled per the package's tsdoc.json", () => {
2424
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
2525
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json')
2626
);
@@ -33,7 +33,7 @@ describe('Extractor-custom-tags', () => {
3333
expect(tsdocConfiguration.isTagSupported(inline)).toBe(true);
3434
expect(tsdocConfiguration.isTagSupported(modifier)).toBe(false);
3535
});
36-
it.only("with standard tags and API Extractor custom tags defined and supported when the package's tsdoc.json extends API Extractor's tsdoc.json", () => {
36+
it("with standard tags and API Extractor custom tags defined and supported when the package's tsdoc.json extends API Extractor's tsdoc.json", () => {
3737
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
3838
path.join(testDataFolder, 'custom-tsdoc-tags/api-extractor.json')
3939
);

apps/api-extractor/src/api/test/ExtractorConfig-lookup.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ function expectEqualPaths(path1: string, path2: string): void {
1616

1717
// Tests for expanding the "<lookup>" token for the "projectFolder" setting in api-extractor.json
1818
describe(`${ExtractorConfig.name}.${ExtractorConfig.loadFileAndPrepare.name}`, () => {
19-
it.only('config-lookup1: looks up ./api-extractor.json', () => {
19+
it('config-lookup1: looks up ./api-extractor.json', () => {
2020
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
2121
path.join(testDataFolder, 'config-lookup1/api-extractor.json')
2222
);
2323
expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup1'));
2424
});
25-
it.only('config-lookup2: looks up ./config/api-extractor.json', () => {
25+
it('config-lookup2: looks up ./config/api-extractor.json', () => {
2626
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
2727
path.join(testDataFolder, 'config-lookup2/config/api-extractor.json')
2828
);
2929
expectEqualPaths(extractorConfig.projectFolder, path.join(testDataFolder, 'config-lookup2'));
3030
});
31-
it.only('config-lookup3a: looks up ./src/test/config/api-extractor.json', () => {
31+
it('config-lookup3a: looks up ./src/test/config/api-extractor.json', () => {
3232
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
3333
path.join(testDataFolder, 'config-lookup3/src/test/config/api-extractor.json')
3434
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import * as path from 'path';
5+
6+
import { ExtractorConfig } from '../ExtractorConfig';
7+
8+
const testDataFolder: string = path.join(__dirname, 'test-data');
9+
10+
describe('ExtractorConfig-tagsToReport', () => {
11+
it('tagsToReport merge correctly', () => {
12+
const extractorConfig: ExtractorConfig = ExtractorConfig.loadFileAndPrepare(
13+
path.join(testDataFolder, 'tags-to-report/api-extractor.json')
14+
);
15+
const { tagsToReport } = extractorConfig;
16+
expect(tagsToReport).toEqual({
17+
'@deprecated': true,
18+
'@eventProperty': true,
19+
'@myCustomTag': true,
20+
'@myCustomTag2': false,
21+
'@override': false,
22+
'@sealed': true,
23+
'@virtual': true
24+
});
25+
});
26+
it('Invalid tagsToReport values', () => {
27+
const expectedErrorMessage = `"tagsToReport" contained one or more invalid tags:
28+
\t- @public: Release tags are always included in API reports and must not be specified
29+
\t- @-invalid-tag-2: A TSDoc tag name must start with a letter and contain only letters and numbers`;
30+
expect(() =>
31+
ExtractorConfig.loadFileAndPrepare(
32+
path.join(testDataFolder, 'invalid-tags-to-report/api-extractor.json')
33+
)
34+
).toThrowError(expectedErrorMessage);
35+
});
36+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test case to ensure that merging of `apiReport.tagsToReport` is correct.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "../../../../schemas/api-extractor.schema.json",
3+
4+
"mainEntryPointFilePath": "index.d.ts",
5+
6+
"apiReport": {
7+
"enabled": true,
8+
"tagsToReport": {
9+
"@validTag1": true, // Valid custom tag
10+
"@-invalid-tag-2": true, // Invalid tag - invalid characters
11+
"@public": false, // Release tags must not be specified
12+
"@override": false // Valid (override base tag)
13+
}
14+
},
15+
16+
"docModel": {
17+
"enabled": true
18+
},
19+
20+
"dtsRollup": {
21+
"enabled": true
22+
}
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// empty file
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "tags-to-report",
3+
"version": "1.0.0"
4+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test case to ensure that merging of `apiReport.tagsToReport` is correct.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
3+
4+
"mainEntryPointFilePath": "index.d.ts",
5+
6+
"apiReport": {
7+
"enabled": true
8+
},
9+
10+
"docModel": {
11+
"enabled": true
12+
},
13+
14+
"dtsRollup": {
15+
"enabled": true
16+
}
17+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"extends": "./api-extractor-base.json",
3+
4+
"apiReport": {
5+
"tagsToReport": {
6+
"@myCustomTag": true, // Enable reporting of custom tag
7+
"@override": false, // Disable default reporting of `@override` tag
8+
"@myCustomTag2": false // Disable reporting of custom tag (not included by base config)
9+
}
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// empty file
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "tags-to-report",
3+
"version": "1.0.0"
4+
}

0 commit comments

Comments
 (0)