UNPKG

@microsoft/api-extractor

Version:

Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups

290 lines 16.4 kB
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. import * as path from 'node:path'; import * as semver from 'semver'; import * as ts from 'typescript'; import * as resolve from 'resolve'; import { TSDocConfigFile } from '@microsoft/tsdoc-config'; import { FileSystem, NewlineKind, PackageJsonLookup, Path } from '@rushstack/node-core-library'; import { ExtractorConfig } from './ExtractorConfig'; import { Collector } from '../collector/Collector'; import { DtsRollupGenerator, DtsRollupKind } from '../generators/DtsRollupGenerator'; import { ApiModelGenerator } from '../generators/ApiModelGenerator'; import { ApiReportGenerator } from '../generators/ApiReportGenerator'; import { PackageMetadataManager } from '../analyzer/PackageMetadataManager'; import { ValidationEnhancer } from '../enhancers/ValidationEnhancer'; import { DocCommentEnhancer } from '../enhancers/DocCommentEnhancer'; import { CompilerState } from './CompilerState'; import { MessageRouter } from '../collector/MessageRouter'; import { ConsoleMessageId } from './ConsoleMessageId'; import { SourceMapper } from '../collector/SourceMapper'; /** * This object represents the outcome of an invocation of API Extractor. * * @public */ export class ExtractorResult { /** @internal */ constructor(properties) { this.compilerState = properties.compilerState; this.extractorConfig = properties.extractorConfig; this.succeeded = properties.succeeded; this.apiReportChanged = properties.apiReportChanged; this.errorCount = properties.errorCount; this.warningCount = properties.warningCount; } } /** * The starting point for invoking the API Extractor tool. * @public */ export class Extractor { /** * Returns the version number of the API Extractor NPM package. */ static get version() { return Extractor._getPackageJson().version; } /** * Returns the package name of the API Extractor NPM package. */ static get packageName() { return Extractor._getPackageJson().name; } static _getPackageJson() { return PackageJsonLookup.loadOwnPackageJson(__dirname); } /** * Load the api-extractor.json config file from the specified path, and then invoke API Extractor. */ static loadConfigAndInvoke(configFilePath, options) { const extractorConfig = ExtractorConfig.loadFileAndPrepare(configFilePath); return Extractor.invoke(extractorConfig, options); } /** * Invoke API Extractor using an already prepared `ExtractorConfig` object. */ static invoke(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 !== null && options !== void 0 ? options : {}; const sourceMapper = new SourceMapper(); const messageRouter = new MessageRouter({ workingPackageFolder: packageFolder, messageCallback, messagesConfig: messages || {}, showVerboseMessages, showDiagnostics, tsdocConfiguration, sourceMapper }); if (tsdocConfigFilePath && !tsdocConfigFileNotFound) { if (!Path.isEqual(tsdocConfigFilePath, ExtractorConfig._tsdocBaseFilePath)) { messageRouter.logVerbose(ConsoleMessageId.UsingCustomTSDocConfig, `Using custom TSDoc config from ${tsdocConfigFilePath}`); } } this._checkCompilerCompatibility(extractorConfig, messageRouter); if (messageRouter.showDiagnostics) { messageRouter.logDiagnostic(''); messageRouter.logDiagnosticHeader('Final prepared ExtractorConfig'); messageRouter.logDiagnostic(extractorConfig.getDiagnosticDump()); messageRouter.logDiagnosticFooter(); messageRouter.logDiagnosticHeader('Compiler options'); const serializedCompilerOptions = MessageRouter.buildJsonDumpObject(compilerState.program.getCompilerOptions()); messageRouter.logDiagnostic(JSON.stringify(serializedCompilerOptions, undefined, 2)); messageRouter.logDiagnosticFooter(); messageRouter.logDiagnosticHeader('TSDoc configuration'); // Convert the TSDocConfiguration into a tsdoc.json representation const combinedConfigFile = TSDocConfigFile.loadFromParser(tsdocConfiguration); const serializedTSDocConfig = MessageRouter.buildJsonDumpObject(combinedConfigFile.saveToObject()); messageRouter.logDiagnostic(JSON.stringify(serializedTSDocConfig, undefined, 2)); messageRouter.logDiagnosticFooter(); } const collector = new Collector({ program: compilerState.program, messageRouter, extractorConfig, sourceMapper }); collector.analyze(); DocCommentEnhancer.analyze(collector); ValidationEnhancer.analyze(collector); const modelBuilder = new ApiModelGenerator(collector, extractorConfig); const apiPackage = modelBuilder.buildApiPackage(); if (messageRouter.showDiagnostics) { messageRouter.logDiagnostic(''); // skip a line after any diagnostic messages } if (modelBuilder.docModelEnabled) { messageRouter.logVerbose(ConsoleMessageId.WritingDocModelFile, `Writing: ${apiJsonFilePath}`); apiPackage.saveToJsonFile(apiJsonFilePath, { toolPackage: Extractor.packageName, toolVersion: Extractor.version, newlineConversion: newlineKind, ensureFolderExists: true, testMode }); } function writeApiReport(reportConfig) { return Extractor._writeApiReport(collector, extractorConfig, messageRouter, reportTempFolder, reportFolder, reportConfig, localBuild, printApiReportDiff); } let anyReportChanged = false; if (apiReportEnabled) { for (const reportConfig of reportConfigs) { anyReportChanged = writeApiReport(reportConfig) || anyReportChanged; } } if (rollupEnabled) { Extractor._generateRollupDtsFile(collector, publicTrimmedFilePath, DtsRollupKind.PublicRelease, newlineKind); Extractor._generateRollupDtsFile(collector, alphaTrimmedFilePath, DtsRollupKind.AlphaRelease, newlineKind); Extractor._generateRollupDtsFile(collector, betaTrimmedFilePath, DtsRollupKind.BetaRelease, newlineKind); Extractor._generateRollupDtsFile(collector, untrimmedFilePath, DtsRollupKind.InternalRelease, newlineKind); } if (tsdocMetadataEnabled) { // Write the tsdoc-metadata.json file for this project PackageMetadataManager.writeTsdocMetadataFile(tsdocMetadataFilePath, newlineKind); } // Show all the messages that we collected during analysis messageRouter.handleRemainingNonConsoleMessages(); // Determine success let succeeded; if (localBuild) { // For a local build, fail if there were errors (but ignore warnings) succeeded = messageRouter.errorCount === 0; } else { // For a production build, fail if there were any errors or warnings succeeded = messageRouter.errorCount + messageRouter.warningCount === 0; } return new ExtractorResult({ compilerState, extractorConfig, succeeded, 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}. * @param printApiReportDiff - {@link IExtractorInvokeOptions.printApiReportDiff} * * @returns Whether or not the newly generated report differs from the existing report (if one exists). */ static _writeApiReport(collector, extractorConfig, messageRouter, reportTempDirectoryPath, reportDirectoryPath, reportConfig, localBuild, printApiReportDiff) { let apiReportChanged = false; const actualApiReportPath = path.resolve(reportTempDirectoryPath, reportConfig.fileName); const actualApiReportShortPath = extractorConfig._getShortFilePath(actualApiReportPath); const expectedApiReportPath = path.resolve(reportDirectoryPath, reportConfig.fileName); const expectedApiReportShortPath = extractorConfig._getShortFilePath(expectedApiReportPath); collector.messageRouter.logVerbose(ConsoleMessageId.WritingApiReport, `Generating ${reportConfig.variant} API report: ${expectedApiReportPath}`); const actualApiReportContent = 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 = FileSystem.readFile(expectedApiReportPath, { convertLineEndings: NewlineKind.Lf }); 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 }); } if (messageRouter.showVerboseMessages || printApiReportDiff) { const Diff = require('diff'); const patch = Diff.structuredPatch(expectedApiReportShortPath, actualApiReportShortPath, expectedApiReportContent, actualApiReportContent); const logFunction = 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, `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 = 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; } static _checkCompilerCompatibility(extractorConfig, messageRouter) { messageRouter.logInfo(ConsoleMessageId.Preamble, `Analysis will use the bundled TypeScript version ${ts.version}`); try { const typescriptPath = resolve.sync('typescript', { basedir: extractorConfig.projectFolder, preserveSymlinks: false }); const packageJsonLookup = new PackageJsonLookup(); const packageJson = packageJsonLookup.tryLoadNodePackageJsonFor(typescriptPath); if (packageJson && packageJson.version && semver.valid(packageJson.version)) { // Consider a newer MINOR release to be incompatible const ourMajor = semver.major(ts.version); const ourMinor = semver.minor(ts.version); const theirMajor = semver.major(packageJson.version); const theirMinor = semver.minor(packageJson.version); if (theirMajor > ourMajor || (theirMajor === ourMajor && theirMinor > ourMinor)) { messageRouter.logInfo(ConsoleMessageId.CompilerVersionNotice, `*** The target project appears to use TypeScript ${packageJson.version} which is newer than the` + ` bundled compiler engine; consider upgrading API Extractor.`); } } } catch (e) { // The compiler detection heuristic is not expected to work in many configurations } } static _generateRollupDtsFile(collector, outputPath, dtsKind, newlineKind) { if (outputPath !== '') { collector.messageRouter.logVerbose(ConsoleMessageId.WritingDtsRollup, `Writing package typings: ${outputPath}`); DtsRollupGenerator.writeTypingsFile(collector, outputPath, dtsKind, newlineKind); } } } //# sourceMappingURL=Extractor.js.map