UNPKG

@microsoft/api-extractor

Version:

Validate, document, and review the exported API for a TypeScript library

187 lines (185 loc) 10.1 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. Object.defineProperty(exports, "__esModule", { value: true }); const fsx = require("fs-extra"); const path = require("path"); const ts = require("typescript"); const lodash = require("lodash"); const colors = require("colors"); const node_core_library_1 = require("@microsoft/node-core-library"); const ExtractorContext_1 = require("../ExtractorContext"); const ApiJsonGenerator_1 = require("../generators/ApiJsonGenerator"); const ApiFileGenerator_1 = require("../generators/ApiFileGenerator"); const MonitoredLogger_1 = require("./MonitoredLogger"); /** * Used to invoke the API Extractor tool. * @public */ class Extractor { constructor(config, options) { let mergedLogger; if (options && options.customLogger) { mergedLogger = lodash.merge(lodash.clone(Extractor._defaultLogger), options.customLogger); } else { mergedLogger = Extractor._defaultLogger; } this._monitoredLogger = new MonitoredLogger_1.MonitoredLogger(mergedLogger); this._config = Extractor._applyConfigDefaults(config); this._monitoredLogger.logVerbose('API Extractor Config: ' + JSON.stringify(this._config)); if (!options) { options = {}; } this._localBuild = options.localBuild || false; switch (this._config.compiler.configType) { case 'tsconfig': const rootFolder = this._config.compiler.rootFolder; if (!fsx.existsSync(rootFolder)) { throw new Error('The root folder does not exist: ' + rootFolder); } this._absoluteRootFolder = path.normalize(path.resolve(rootFolder)); let tsconfig = this._config.compiler.overrideTsconfig; if (!tsconfig) { // If it wasn't overridden, then load it from disk tsconfig = node_core_library_1.JsonFile.load(path.join(this._absoluteRootFolder, 'tsconfig.json')); } const commandLine = ts.parseJsonConfigFileContent(tsconfig, ts.sys, this._absoluteRootFolder); this._program = ts.createProgram(commandLine.fileNames, commandLine.options); if (commandLine.errors.length > 0) { throw new Error('Error parsing tsconfig.json content: ' + commandLine.errors[0].messageText); } break; case 'runtime': if (!options.compilerProgram) { throw new Error('The compiler.configType=runtime configuration was specified,' + ' but the caller did not provide an options.compilerProgram object'); } this._program = options.compilerProgram; const rootDir = this._program.getCompilerOptions().rootDir; if (!rootDir) { throw new Error('The provided compiler state does not specify a root folder'); } if (!fsx.existsSync(rootDir)) { throw new Error('The rootDir does not exist: ' + rootDir); } this._absoluteRootFolder = path.resolve(rootDir); break; default: throw new Error('Unsupported config type'); } } static _applyConfigDefaults(config) { // Use the provided config to override the defaults const normalized = lodash.merge(lodash.cloneDeep(Extractor._defaultConfig), config); return normalized; } /** * Invokes the API Extractor engine, using the configuration that was passed to the constructor. * @deprecated Use {@link Extractor.processProject} instead. */ analyzeProject(options) { this.processProject(options); } /** * Invokes the API Extractor engine, using the configuration that was passed to the constructor. * @param options - provides additional runtime state that is NOT part of the API Extractor * config file. * @returns true if there were no errors or warnings; false if the tool chain should fail the build */ processProject(options) { this._monitoredLogger.resetCounters(); if (!options) { options = {}; } const projectConfig = options.projectConfig ? options.projectConfig : this._config.project; // This helps strict-null-checks to understand that _applyConfigDefaults() eliminated // any undefined members if (!(this._config.policies && this._config.apiJsonFile && this._config.apiReviewFile)) { throw new Error('The configuration object wasn\'t normalized properly'); } const context = new ExtractorContext_1.ExtractorContext({ program: this._program, entryPointFile: path.resolve(this._absoluteRootFolder, projectConfig.entryPointSourceFile), logger: this._monitoredLogger, policies: this._config.policies }); for (const externalJsonFileFolder of projectConfig.externalJsonFileFolders || []) { context.loadExternalPackages(path.resolve(this._absoluteRootFolder, externalJsonFileFolder)); } const packageBaseName = path.basename(context.packageName); const apiJsonFileConfig = this._config.apiJsonFile; if (apiJsonFileConfig.enabled) { const outputFolder = path.resolve(this._absoluteRootFolder, apiJsonFileConfig.outputFolder); fsx.mkdirsSync(outputFolder); const jsonGenerator = new ApiJsonGenerator_1.default(); const apiJsonFilename = path.join(outputFolder, packageBaseName + '.api.json'); this._monitoredLogger.logVerbose('Writing: ' + apiJsonFilename); jsonGenerator.writeJsonFile(apiJsonFilename, context); } if (this._config.apiReviewFile.enabled) { const generator = new ApiFileGenerator_1.default(); const apiReviewFilename = packageBaseName + '.api.ts'; const actualApiReviewPath = path.resolve(this._absoluteRootFolder, this._config.apiReviewFile.tempFolder, apiReviewFilename); const actualApiReviewShortPath = this._getShortFilePath(actualApiReviewPath); const expectedApiReviewPath = path.resolve(this._absoluteRootFolder, this._config.apiReviewFile.apiReviewFolder, apiReviewFilename); const expectedApiReviewShortPath = this._getShortFilePath(expectedApiReviewPath); const actualApiReviewContent = generator.generateApiFileContent(context); // Write the actual file fsx.mkdirsSync(path.dirname(actualApiReviewPath)); fsx.writeFileSync(actualApiReviewPath, actualApiReviewContent); // Compare it against the expected file if (fsx.existsSync(expectedApiReviewPath)) { const expectedApiReviewContent = fsx.readFileSync(expectedApiReviewPath).toString(); if (!ApiFileGenerator_1.default.areEquivalentApiFileContents(actualApiReviewContent, expectedApiReviewContent)) { if (!this._localBuild) { // For production, issue a warning that will break the CI build. this._monitoredLogger.logWarning('You have changed the public API signature for this project.' + ` Please overwrite ${expectedApiReviewShortPath} with a` + ` copy of ${actualApiReviewShortPath}` + ' and then request an API review. See the Git repository README.md for more info.'); } else { // For a local build, just copy the file automatically. this._monitoredLogger.logWarning('You have changed the public API signature for this project.' + ` Updating ${expectedApiReviewShortPath}`); fsx.writeFileSync(expectedApiReviewPath, actualApiReviewContent); } } else { this._monitoredLogger.logVerbose(`The API signature is up to date: ${actualApiReviewShortPath}`); } } else { // NOTE: This warning seems like a nuisance, but it has caught genuine mistakes. // For example, when projects were moved into category folders, the relative path for // the API review files ended up in the wrong place. this._monitoredLogger.logError(`The API review file has not been set up.` + ` Do this by copying ${actualApiReviewShortPath}` + ` to ${expectedApiReviewShortPath} and committing it.`); } } // If there were any errors or warnings, then fail the build return (this._monitoredLogger.errorCount + this._monitoredLogger.warningCount) === 0; } _getShortFilePath(absolutePath) { if (!path.isAbsolute(absolutePath)) { throw new Error('Expected absolute path: ' + absolutePath); } return path.relative(this._absoluteRootFolder, absolutePath).replace(/\\/g, '/'); } } /** * The JSON Schema for API Extractor config file (api-extractor-config.schema.json). */ Extractor.jsonSchema = node_core_library_1.JsonSchema.fromFile(path.join(__dirname, './api-extractor.schema.json')); Extractor._defaultConfig = node_core_library_1.JsonFile.load(path.join(__dirname, './api-extractor-defaults.json')); Extractor._defaultLogger = { logVerbose: (message) => console.log('(Verbose) ' + message), logInfo: (message) => console.log(message), logWarning: (message) => console.warn(colors.yellow(message)), logError: (message) => console.error(colors.red(message)) }; exports.Extractor = Extractor; //# sourceMappingURL=Extractor.js.map