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] Allow generating API reports for different release levels #4621

Merged
merged 51 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
429c22a
refactor: Update ApiReportGenerator to support a minimum release level
Josmithr Mar 25, 2024
5d2fd3b
refactor: [WIP] Add config entries for new report types
Josmithr Mar 26, 2024
be2303b
refactor: Finish implementing config mapping
Josmithr Apr 2, 2024
1160d56
Merge branch 'main' into emit-report-per-release-level
Josmithr Apr 2, 2024
58b155f
refactor: Update contract and add missing docs
Josmithr Apr 2, 2024
5d312cf
docs: Fix comment
Josmithr Apr 2, 2024
46f0415
build: Update config JSON schema
Josmithr Apr 2, 2024
4142ab1
fix: Restore correct handling of untrimmed report generation
Josmithr Apr 2, 2024
e50765f
fix: Linter violation
Josmithr Apr 2, 2024
798f85d
improvement: Runtime validation to ensure no duplicate report names a…
Josmithr Apr 2, 2024
c9658d3
fix: Don't emit comments for items that should not be included in the…
Josmithr Apr 2, 2024
c3d6d45
test: Add tests
Josmithr Apr 2, 2024
d394cfa
docs: Add change entry
Josmithr Apr 2, 2024
e212c59
refactor: Introduce API report variant pattern
Josmithr Apr 5, 2024
75d118d
docs: Add docs
Josmithr Apr 15, 2024
342d044
docs: Expand comment
Josmithr Apr 15, 2024
6ea754d
remove: Unneeded eslint disable pragma
Josmithr Apr 15, 2024
f843700
refactor: Private static method
Josmithr Apr 15, 2024
8122bfc
refactor: Remove options interface
Josmithr Apr 15, 2024
b7d9224
refactor: Actually remove interface
Josmithr Apr 15, 2024
13d6409
refactor: Simplify extension handling
Josmithr Apr 15, 2024
cc54986
docs: Loosen contract
Josmithr Apr 15, 2024
f0eb526
refactor: Inline validation
Josmithr Apr 15, 2024
c9dde7b
fix: Add missing export
Josmithr Apr 15, 2024
b116de6
test: Update file name pattern to test extension preservation
Josmithr Apr 15, 2024
72ba5c0
remove: Obsolete test asset
Josmithr Apr 15, 2024
24f0c4b
improvement: Better variant string casing in report generation
Josmithr Apr 15, 2024
c3a8b4b
docs: Add missing release tag
Josmithr Apr 16, 2024
0209edd
Merge remote-tracking branch 'remotes/origin/main' into emit-report-p…
octogonz May 14, 2024
f236886
docs: Update comment
Josmithr May 14, 2024
838c191
docs: Update comment
Josmithr May 14, 2024
b6ffa03
docs: Update description
Josmithr May 14, 2024
741dfc6
Merge branch 'emit-report-per-release-level' of https://github.com/Jo…
Josmithr May 14, 2024
f5971e5
docs: Update default value docs
Josmithr May 14, 2024
f7e1760
docs: Update description
Josmithr May 14, 2024
d8850b8
refactor: Update `reportFileName` policy and related documentation
Josmithr May 22, 2024
dbc179b
Merge branch 'main' into emit-report-per-release-level
Josmithr May 22, 2024
f8914b7
fix: Config
Josmithr May 22, 2024
e0cf8a1
refactor: Fix logging ID
Josmithr May 22, 2024
751cc7e
fix: Comment and punctuation
Josmithr May 22, 2024
6d62b62
refactor: Simplify params
Josmithr May 22, 2024
28d9293
docs: Update API report
Josmithr May 22, 2024
920523f
refactor: Remove unused import
Josmithr May 22, 2024
2c2a00b
refactor: Rename folder properties
Josmithr May 23, 2024
8e97e92
refactor: Rename interface
Josmithr May 23, 2024
d66e8be
refactor: Restore legacy properties as deprecated
Josmithr May 23, 2024
ed8cd93
fix: Argument ordering
Josmithr May 23, 2024
3b99b09
refactor: Rename variables to match corresponding property names
Josmithr May 23, 2024
229c311
refactor: Fix package export name
Josmithr May 23, 2024
022ab69
docs: Mark `ExtractorConfig` as `@sealed`
Josmithr May 23, 2024
2108713
refactor: Rename variable per conventions
Josmithr May 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
231 changes: 138 additions & 93 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { MessageRouter } from '../collector/MessageRouter';
import { ConsoleMessageId } from './ConsoleMessageId';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import { SourceMapper } from '../collector/SourceMapper';
import { ApiReportVariant } from './IConfigFile';

/**
* Runtime options for Extractor.
Expand Down Expand Up @@ -285,100 +286,23 @@ export class Extractor {
});
}

let apiReportChanged: boolean = false;
octogonz marked this conversation as resolved.
Show resolved Hide resolved

if (extractorConfig.apiReportEnabled) {
const actualApiReportPath: string = extractorConfig.reportTempFilePath;
const actualApiReportShortPath: string = extractorConfig._getShortFilePath(
extractorConfig.reportTempFilePath
);

const expectedApiReportPath: string = extractorConfig.reportFilePath;
const expectedApiReportShortPath: string = extractorConfig._getShortFilePath(
extractorConfig.reportFilePath
function writeApiReport(reportFileName: string, reportVariant: ApiReportVariant): boolean {
return Extractor._writeApiReport(
collector,
extractorConfig,
messageRouter,
extractorConfig.reportDirectoryPath,
extractorConfig.reportTempDirectoryPath,
reportFileName,
reportVariant,
localBuild
);
}

const actualApiReportContent: string = ApiReportGenerator.generateReviewFileContent(collector);

// Write the actual file
FileSystem.writeFile(actualApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind
});

// Compare it against the expected file
if (FileSystem.exists(expectedApiReportPath)) {
const expectedApiReportContent: string = FileSystem.readFile(expectedApiReportPath);

if (
!ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent)
) {
apiReportChanged = true;

if (!localBuild) {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'You have changed the public API signature for this project.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`
);
} else {
// For a local build, just copy the file automatically.
messageRouter.logWarning(
ConsoleMessageId.ApiReportCopied,
'You have changed the public API signature for this project.' +
` Updating ${expectedApiReportShortPath}`
);

FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind
});
}
} else {
messageRouter.logVerbose(
ConsoleMessageId.ApiReportUnchanged,
`The API report is up to date: ${actualApiReportShortPath}`
);
}
} else {
// The target file does not exist, so we are setting up the API review file for the first time.
//
// NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder"
// setting, which causes a new file to silently get written to the wrong place. This can be confusing.
// Thus we treat the initial creation of the file specially.
apiReportChanged = true;

if (!localBuild) {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'The API report file is missing.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`
);
} else {
const expectedApiReportFolder: string = path.dirname(expectedApiReportPath);
if (!FileSystem.exists(expectedApiReportFolder)) {
messageRouter.logError(
ConsoleMessageId.ApiReportFolderMissing,
'Unable to create the API report file. Please make sure the target folder exists:\n' +
expectedApiReportFolder
);
} else {
FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
convertLineEndings: extractorConfig.newlineKind
});
messageRouter.logWarning(
ConsoleMessageId.ApiReportCreated,
'The API report file was missing, so a new file was created. Please add this file to Git:\n' +
expectedApiReportPath
);
}
}
let anyReportChanged: boolean = false;
if (extractorConfig.apiReportEnabled) {
for (const reportConfig of extractorConfig.reportConfigs) {
anyReportChanged = writeApiReport(reportConfig.fileName, reportConfig.variant) || anyReportChanged;
}
}

Expand Down Expand Up @@ -434,12 +358,133 @@ export class Extractor {
compilerState,
extractorConfig,
succeeded,
apiReportChanged,
apiReportChanged: anyReportChanged,
errorCount: messageRouter.errorCount,
warningCount: messageRouter.warningCount
});
}

/**
* Generates the API report at the specified release level, writes it to the specified file path, and compares
* the output to the existing report (if one exists).
*
* @param reportTempDirectoryPath - The path to the directory under which the temp report file will be written prior
* to comparison with an existing report.
* @param reportDirectoryPath - The path to the directory under which the existing report file is located, and to
* which the new report will be written post-comparison.
* @param reportVariant - Determines which API members will be included in the report, based on their tagged release levels.
*
* @returns Whether or not the newly generated report differs from the existing report (if one exists).
*/
private static _writeApiReport(
collector: Collector,
extractorConfig: ExtractorConfig,
messageRouter: MessageRouter,
reportTempDirectoryPath: string,
reportDirectoryPath: string,
reportFileName: string,
reportVariant: ApiReportVariant,
localBuild: boolean
): boolean {
let apiReportChanged: boolean = false;
octogonz marked this conversation as resolved.
Show resolved Hide resolved

const actualApiReportPath: string = path.resolve(reportTempDirectoryPath, reportFileName);
const actualApiReportShortPath: string = extractorConfig._getShortFilePath(actualApiReportPath);

const expectedApiReportPath: string = path.resolve(reportDirectoryPath, reportFileName);
const expectedApiReportShortPath: string = extractorConfig._getShortFilePath(expectedApiReportPath);

collector.messageRouter.logVerbose(
ConsoleMessageId.WritingDtsRollup,
`Generating ${reportVariant} API report : ${expectedApiReportPath}`
);

const actualApiReportContent: string = ApiReportGenerator.generateReviewFileContent(collector, {
reportVariant: reportVariant
});

// Write the actual file
FileSystem.writeFile(actualApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind
});

// Compare it against the expected file
if (FileSystem.exists(expectedApiReportPath)) {
const expectedApiReportContent: string = FileSystem.readFile(expectedApiReportPath);

if (
!ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent)
) {
apiReportChanged = true;

if (!localBuild) {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'You have changed the API signature for this project.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`
);
} else {
// For a local build, just copy the file automatically.
messageRouter.logWarning(
ConsoleMessageId.ApiReportCopied,
`You have changed the API signature for this project. Updating ${expectedApiReportShortPath}`
);

FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
ensureFolderExists: true,
convertLineEndings: extractorConfig.newlineKind
});
}
} else {
messageRouter.logVerbose(
ConsoleMessageId.ApiReportUnchanged,
`The API report is up to date: ${actualApiReportShortPath}`
);
}
} else {
// The target file does not exist, so we are setting up the API review file for the first time.
//
// NOTE: People sometimes make a mistake where they move a project and forget to update the "reportFolder"
// setting, which causes a new file to silently get written to the wrong place. This can be confusing.
// Thus we treat the initial creation of the file specially.
apiReportChanged = true;

if (!localBuild) {
// For a production build, issue a warning that will break the CI build.
messageRouter.logWarning(
ConsoleMessageId.ApiReportNotCopied,
'The API report file is missing.' +
` Please copy the file "${actualApiReportShortPath}" to "${expectedApiReportShortPath}",` +
` or perform a local build (which does this automatically).` +
` See the Git repo documentation for more info.`
);
} else {
const expectedApiReportFolder: string = path.dirname(expectedApiReportPath);
if (!FileSystem.exists(expectedApiReportFolder)) {
messageRouter.logError(
ConsoleMessageId.ApiReportFolderMissing,
'Unable to create the API report file. Please make sure the target folder exists:\n' +
expectedApiReportFolder
);
} else {
FileSystem.writeFile(expectedApiReportPath, actualApiReportContent, {
convertLineEndings: extractorConfig.newlineKind
});
messageRouter.logWarning(
ConsoleMessageId.ApiReportCreated,
'The API report file was missing, so a new file was created. Please add this file to Git:\n' +
expectedApiReportPath
);
}
}
}
return apiReportChanged;
}

private static _checkCompilerCompatibility(
extractorConfig: ExtractorConfig,
messageRouter: MessageRouter
Expand Down