Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/api-extractor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@rushstack/rig-package": "workspace:*",
"@rushstack/terminal": "workspace:*",
"@rushstack/ts-command-line": "workspace:*",
"diff": "~8.0.2",
"lodash": "~4.17.15",
"minimatch": "10.0.3",
"resolve": "~1.22.1",
Expand Down
6 changes: 6 additions & 0 deletions apps/api-extractor/src/api/ConsoleMessageId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export enum ConsoleMessageId {
*/
ApiReportNotCopied = 'console-api-report-not-copied',

/**
* Changes to the API report:
* ___
*/
ApiReportDiff = 'console-api-report-diff',

/**
* "You have changed the public API signature for this project. Updating ___"
*/
Expand Down
148 changes: 95 additions & 53 deletions apps/api-extractor/src/api/Extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { ApiPackage } from '@microsoft/api-extractor-model';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import {
FileSystem,
type NewlineKind,
NewlineKind,
PackageJsonLookup,
type IPackageJson,
type INodePackageJson,
Expand Down Expand Up @@ -91,6 +91,15 @@ export interface IExtractorInvokeOptions {
* the STDERR/STDOUT console.
*/
messageCallback?: (message: ExtractorMessage) => void;

/**
* If true, then any differences between the actual and expected API reports will be
* printed on the console.
*
* @remarks
* The diff is not printed if the expected API report file has not been created yet.
*/
printApiReportDiff?: boolean;
}

/**
Expand Down Expand Up @@ -192,36 +201,52 @@ export class Extractor {
* Invoke API Extractor using an already prepared `ExtractorConfig` object.
*/
public static invoke(extractorConfig: ExtractorConfig, options?: IExtractorInvokeOptions): ExtractorResult {
if (!options) {
options = {};
}

const localBuild: boolean = options.localBuild || false;

let compilerState: CompilerState | undefined;
if (options.compilerState) {
compilerState = options.compilerState;
} else {
compilerState = CompilerState.create(extractorConfig, options);
}
const {
packageFolder,
messages,
tsdocConfiguration,
tsdocConfigFile: { filePath: tsdocConfigFilePath, fileNotFound: tsdocConfigFileNotFound },
apiJsonFilePath,
newlineKind,
reportTempFolder,
reportFolder,
apiReportEnabled,
reportConfigs,
testMode,
rollupEnabled,
publicTrimmedFilePath,
alphaTrimmedFilePath,
betaTrimmedFilePath,
untrimmedFilePath,
tsdocMetadataEnabled,
tsdocMetadataFilePath
} = extractorConfig;
const {
localBuild = false,
compilerState = CompilerState.create(extractorConfig, options),
messageCallback,
showVerboseMessages = false,
showDiagnostics = false,
printApiReportDiff = false
} = options ?? {};

const sourceMapper: SourceMapper = new SourceMapper();

const messageRouter: MessageRouter = new MessageRouter({
workingPackageFolder: extractorConfig.packageFolder,
messageCallback: options.messageCallback,
messagesConfig: extractorConfig.messages || {},
showVerboseMessages: !!options.showVerboseMessages,
showDiagnostics: !!options.showDiagnostics,
tsdocConfiguration: extractorConfig.tsdocConfiguration,
workingPackageFolder: packageFolder,
messageCallback,
messagesConfig: messages || {},
showVerboseMessages,
showDiagnostics,
tsdocConfiguration,
sourceMapper
});

if (extractorConfig.tsdocConfigFile.filePath && !extractorConfig.tsdocConfigFile.fileNotFound) {
if (!Path.isEqual(extractorConfig.tsdocConfigFile.filePath, ExtractorConfig._tsdocBaseFilePath)) {
if (tsdocConfigFilePath && !tsdocConfigFileNotFound) {
if (!Path.isEqual(tsdocConfigFilePath, ExtractorConfig._tsdocBaseFilePath)) {
messageRouter.logVerbose(
ConsoleMessageId.UsingCustomTSDocConfig,
'Using custom TSDoc config from ' + extractorConfig.tsdocConfigFile.filePath
`Using custom TSDoc config from ${tsdocConfigFilePath}`
);
}
}
Expand All @@ -243,9 +268,7 @@ export class Extractor {

messageRouter.logDiagnosticHeader('TSDoc configuration');
// Convert the TSDocConfiguration into a tsdoc.json representation
const combinedConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(
extractorConfig.tsdocConfiguration
);
const combinedConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(tsdocConfiguration);
const serializedTSDocConfig: object = MessageRouter.buildJsonDumpObject(
combinedConfigFile.saveToObject()
);
Expand Down Expand Up @@ -273,17 +296,14 @@ export class Extractor {
}

if (modelBuilder.docModelEnabled) {
messageRouter.logVerbose(
ConsoleMessageId.WritingDocModelFile,
'Writing: ' + extractorConfig.apiJsonFilePath
);
apiPackage.saveToJsonFile(extractorConfig.apiJsonFilePath, {
messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, `Writing: ${apiJsonFilePath}`);
apiPackage.saveToJsonFile(apiJsonFilePath, {
toolPackage: Extractor.packageName,
toolVersion: Extractor.version,

newlineConversion: extractorConfig.newlineKind,
newlineConversion: newlineKind,
ensureFolderExists: true,
testMode: extractorConfig.testMode
testMode
});
}

Expand All @@ -292,53 +312,51 @@ export class Extractor {
collector,
extractorConfig,
messageRouter,
extractorConfig.reportTempFolder,
extractorConfig.reportFolder,
reportTempFolder,
reportFolder,
reportConfig,
localBuild
localBuild,
printApiReportDiff
);
}

let anyReportChanged: boolean = false;
if (extractorConfig.apiReportEnabled) {
for (const reportConfig of extractorConfig.reportConfigs) {
if (apiReportEnabled) {
for (const reportConfig of reportConfigs) {
anyReportChanged = writeApiReport(reportConfig) || anyReportChanged;
}
}

if (extractorConfig.rollupEnabled) {
if (rollupEnabled) {
Extractor._generateRollupDtsFile(
collector,
extractorConfig.publicTrimmedFilePath,
publicTrimmedFilePath,
DtsRollupKind.PublicRelease,
extractorConfig.newlineKind
newlineKind
);
Extractor._generateRollupDtsFile(
collector,
extractorConfig.alphaTrimmedFilePath,
alphaTrimmedFilePath,
DtsRollupKind.AlphaRelease,
extractorConfig.newlineKind
newlineKind
);
Extractor._generateRollupDtsFile(
collector,
extractorConfig.betaTrimmedFilePath,
betaTrimmedFilePath,
DtsRollupKind.BetaRelease,
extractorConfig.newlineKind
newlineKind
);
Extractor._generateRollupDtsFile(
collector,
extractorConfig.untrimmedFilePath,
untrimmedFilePath,
DtsRollupKind.InternalRelease,
extractorConfig.newlineKind
newlineKind
);
}

if (extractorConfig.tsdocMetadataEnabled) {
if (tsdocMetadataEnabled) {
// Write the tsdoc-metadata.json file for this project
PackageMetadataManager.writeTsdocMetadataFile(
extractorConfig.tsdocMetadataFilePath,
extractorConfig.newlineKind
);
PackageMetadataManager.writeTsdocMetadataFile(tsdocMetadataFilePath, newlineKind);
}

// Show all the messages that we collected during analysis
Expand Down Expand Up @@ -373,6 +391,7 @@ export class Extractor {
* @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}.
* @param printApiReportDiff - {@link IExtractorInvokeOptions.printApiReportDiff}
*
* @returns Whether or not the newly generated report differs from the existing report (if one exists).
*/
Expand All @@ -383,7 +402,8 @@ export class Extractor {
reportTempDirectoryPath: string,
reportDirectoryPath: string,
reportConfig: IExtractorConfigApiReport,
localBuild: boolean
localBuild: boolean,
printApiReportDiff: boolean
): boolean {
let apiReportChanged: boolean = false;

Expand Down Expand Up @@ -411,7 +431,9 @@ export class Extractor {

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

if (
!ApiReportGenerator.areEquivalentApiFileContents(actualApiReportContent, expectedApiReportContent)
Expand Down Expand Up @@ -439,6 +461,26 @@ export class Extractor {
convertLineEndings: extractorConfig.newlineKind
});
}

if (messageRouter.showVerboseMessages || printApiReportDiff) {
const Diff: typeof import('diff') = require('diff');
const patch: import('diff').StructuredPatch = Diff.structuredPatch(
expectedApiReportShortPath,
actualApiReportShortPath,
expectedApiReportContent,
actualApiReportContent
);
const logFunction:
| (typeof MessageRouter.prototype)['logWarning']
| (typeof MessageRouter.prototype)['logVerbose'] = printApiReportDiff
? messageRouter.logWarning.bind(messageRouter)
: messageRouter.logVerbose.bind(messageRouter);

logFunction(
ConsoleMessageId.ApiReportDiff,
'Changes to the API report:\n\n' + Diff.formatPatch(patch)
);
}
} else {
messageRouter.logVerbose(
ConsoleMessageId.ApiReportUnchanged,
Expand Down
36 changes: 23 additions & 13 deletions apps/api-extractor/src/cli/RunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import { ExtractorConfig, type IExtractorConfigPrepareOptions } from '../api/Ext

export class RunAction extends CommandLineAction {
private readonly _configFileParameter: CommandLineStringParameter;
private readonly _localParameter: CommandLineFlagParameter;
private readonly _verboseParameter: CommandLineFlagParameter;
private readonly _localFlag: CommandLineFlagParameter;
private readonly _verboseFlag: CommandLineFlagParameter;
private readonly _diagnosticsParameter: CommandLineFlagParameter;
private readonly _typescriptCompilerFolder: CommandLineStringParameter;
private readonly _typescriptCompilerFolderParameter: CommandLineStringParameter;
private readonly _printApiReportDiffFlag: CommandLineFlagParameter;

public constructor(parser: ApiExtractorCommandLine) {
super({
Expand All @@ -43,7 +44,7 @@ export class RunAction extends CommandLineAction {
description: `Use the specified ${ExtractorConfig.FILENAME} file path, rather than guessing its location`
});

this._localParameter = this.defineFlagParameter({
this._localFlag = this.defineFlagParameter({
parameterLongName: '--local',
parameterShortName: '-l',
description:
Expand All @@ -53,7 +54,7 @@ export class RunAction extends CommandLineAction {
' report file is automatically copied in a local build.'
});

this._verboseParameter = this.defineFlagParameter({
this._verboseFlag = this.defineFlagParameter({
parameterLongName: '--verbose',
parameterShortName: '-v',
description: 'Show additional informational messages in the output.'
Expand All @@ -66,7 +67,7 @@ export class RunAction extends CommandLineAction {
' This flag also enables the "--verbose" flag.'
});

this._typescriptCompilerFolder = this.defineStringParameter({
this._typescriptCompilerFolderParameter = this.defineStringParameter({
parameterLongName: '--typescript-compiler-folder',
argumentName: 'PATH',
description:
Expand All @@ -76,13 +77,21 @@ export class RunAction extends CommandLineAction {
' "--typescriptCompilerFolder" option to specify the folder path where you installed the TypeScript package,' +
" and API Extractor's compiler will use those system typings instead."
});

this._printApiReportDiffFlag = this.defineFlagParameter({
parameterLongName: '--print-api-report-diff',
description:
'If provided, then any differences between the actual and expected API reports will be ' +
'printed on the console. Note that the diff is not printed if the expected API report file has not been ' +
'created yet.'
});
}

protected override async onExecuteAsync(): Promise<void> {
const lookup: PackageJsonLookup = new PackageJsonLookup();
let configFilename: string;

let typescriptCompilerFolder: string | undefined = this._typescriptCompilerFolder.value;
let typescriptCompilerFolder: string | undefined = this._typescriptCompilerFolderParameter.value;
if (typescriptCompilerFolder) {
typescriptCompilerFolder = path.normalize(typescriptCompilerFolder);

Expand All @@ -93,17 +102,17 @@ export class RunAction extends CommandLineAction {
: undefined;
if (!typescriptCompilerPackageJson) {
throw new Error(
`The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a package.`
`The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter is not a package.`
);
} else if (typescriptCompilerPackageJson.name !== 'typescript') {
throw new Error(
`The path specified in the ${this._typescriptCompilerFolder.longName} parameter is not a TypeScript` +
`The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter is not a TypeScript` +
' compiler package.'
);
}
} else {
throw new Error(
`The path specified in the ${this._typescriptCompilerFolder.longName} parameter does not exist.`
`The path specified in the ${this._typescriptCompilerFolderParameter.longName} parameter does not exist.`
);
}
}
Expand Down Expand Up @@ -136,10 +145,11 @@ export class RunAction extends CommandLineAction {
}

const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, {
localBuild: this._localParameter.value,
showVerboseMessages: this._verboseParameter.value,
localBuild: this._localFlag.value,
showVerboseMessages: this._verboseFlag.value,
showDiagnostics: this._diagnosticsParameter.value,
typescriptCompilerFolder: typescriptCompilerFolder
typescriptCompilerFolder: typescriptCompilerFolder,
printApiReportDiff: this._printApiReportDiffFlag.value
});

if (extractorResult.succeeded) {
Expand Down
2 changes: 1 addition & 1 deletion apps/api-extractor/src/collector/MessageRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ export class MessageRouter {

/**
* This returns all remaining messages that were flagged with `addToApiReportFile`, but which were not
* retreieved using `fetchAssociatedMessagesForReviewFile()`.
* retrieved using `fetchAssociatedMessagesForReviewFile()`.
*/
public fetchUnassociatedMessagesForReviewFile(): ExtractorMessage[] {
const messagesForApiReportFile: ExtractorMessage[] = [];
Expand Down
Loading
Loading