Skip to content

Commit

Permalink
Merge pull request #4621 from Josmithr/emit-report-per-release-level
Browse files Browse the repository at this point in the history
[api-extractor] Allow generating API reports for different release levels
  • Loading branch information
octogonz committed May 23, 2024
2 parents d3eebba + 2108713 commit 3afc7bf
Show file tree
Hide file tree
Showing 16 changed files with 481 additions and 171 deletions.
5 changes: 5 additions & 0 deletions apps/api-extractor/src/api/ConsoleMessageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ export enum ConsoleMessageId {
*/
WritingDtsRollup = 'console-writing-dts-rollup',

/**
* "Generating ___ API report: ___"
*/
WritingApiReport = 'console-writing-api-report',

/**
* "You have changed the public API signature for this project.
* Please copy the file ___ to ___, or perform a local build (which does this automatically).
Expand Down
231 changes: 137 additions & 94 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Path
} from '@rushstack/node-core-library';

import { ExtractorConfig } from './ExtractorConfig';
import { ExtractorConfig, type IExtractorConfigApiReport } from './ExtractorConfig';
import { Collector } from '../collector/Collector';
import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator';
import { ApiModelGenerator } from '../generators/ApiModelGenerator';
Expand Down Expand Up @@ -285,100 +285,22 @@ export class Extractor {
});
}

let apiReportChanged: boolean = false;

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(reportConfig: IExtractorConfigApiReport): boolean {
return Extractor._writeApiReport(
collector,
extractorConfig,
messageRouter,
extractorConfig.reportTempFolder,
extractorConfig.reportFolder,
reportConfig,
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) || anyReportChanged;
}
}

Expand Down Expand Up @@ -434,12 +356,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 reportConfig - API report configuration, including its file name and {@link ApiReportVariant}.
*
* @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,
reportConfig: IExtractorConfigApiReport,
localBuild: boolean
): boolean {
let apiReportChanged: boolean = false;

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

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

collector.messageRouter.logVerbose(
ConsoleMessageId.WritingApiReport,
`Generating ${reportConfig.variant} API report: ${expectedApiReportPath}`
);

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

// 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
Loading

0 comments on commit 3afc7bf

Please sign in to comment.