@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
187 lines (185 loc) • 10.1 kB
JavaScript
"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