@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
203 lines (201 loc) • 8.67 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 fs = require("fs");
const AstItem_1 = require("../ast/AstItem");
const AstItemVisitor_1 = require("./AstItemVisitor");
const AstPackage_1 = require("../ast/AstPackage");
const IndentedWriter_1 = require("../IndentedWriter");
const ReleaseTag_1 = require("../aedoc/ReleaseTag");
/**
* For a library such as "example-package", ApiFileGenerator generates the "example-package.api.ts"
* report which is used to detect API changes. The output is pseudocode whose syntax is similar
* but not identical to a "*.d.ts" typings file. The output file is designed to be committed to
* Git with a branch policy that will trigger an API review workflow whenever the file contents
* have changed. For example, the API file indicates *whether* a class has been documented,
* but it does not include the documentation text (since minor text changes should not require
* an API review).
*
* @public
*/
class ApiFileGenerator extends AstItemVisitor_1.default {
constructor() {
super(...arguments);
this._indentedWriter = new IndentedWriter_1.default();
}
/**
* Compares the contents of two API files that were created using ApiFileGenerator,
* and returns true if they are equivalent. Note that these files are not normally edited
* by a human; the "equivalence" comparison here is intended to ignore spurious changes that
* might be introduced by a tool, e.g. Git newline normalization or an editor that strips
* whitespace when saving.
*/
static areEquivalentApiFileContents(actualFileContent, expectedFileContent) {
// NOTE: "\s" also matches "\r" and "\n"
const normalizedActual = actualFileContent.replace(/[\s]+/g, ' ');
const normalizedExpected = expectedFileContent.replace(/[\s]+/g, ' ');
return normalizedActual === normalizedExpected;
}
/**
* Generates the report and writes it to disk.
*
* @param reportFilename - The output filename
* @param analyzer - An Analyzer object representing the input project.
*/
writeApiFile(reportFilename, context) {
const fileContent = this.generateApiFileContent(context);
fs.writeFileSync(reportFilename, fileContent);
}
generateApiFileContent(context) {
this._insideTypeLiteral = 0;
// Normalize to CRLF
this.visit(context.package);
const fileContent = this._indentedWriter.toString().replace(/\r?\n/g, '\r\n');
return fileContent;
}
visitAstStructuredType(astStructuredType) {
const declarationLine = astStructuredType.getDeclarationLine();
if (astStructuredType.documentation.preapproved) {
this._indentedWriter.writeLine('// @internal (preapproved)');
this._indentedWriter.writeLine(declarationLine + ' {');
this._indentedWriter.writeLine('}');
return;
}
if (astStructuredType.kind !== AstItem_1.AstItemKind.TypeLiteral) {
this._writeAedocSynopsis(astStructuredType);
}
this._indentedWriter.writeLine(declarationLine + ' {');
this._indentedWriter.indentScope(() => {
if (astStructuredType.kind === AstItem_1.AstItemKind.TypeLiteral) {
// Type literals don't have normal JSDoc. Write only the warnings,
// and put them after the '{' since the declaration is nested.
this._writeWarnings(astStructuredType);
}
for (const member of astStructuredType.getSortedMemberItems()) {
this.visit(member);
this._indentedWriter.writeLine();
}
});
this._indentedWriter.write('}');
}
visitAstEnum(astEnum) {
this._writeAedocSynopsis(astEnum);
this._indentedWriter.writeLine(`enum ${astEnum.name} {`);
this._indentedWriter.indentScope(() => {
const members = astEnum.getSortedMemberItems();
for (let i = 0; i < members.length; ++i) {
this.visit(members[i]);
this._indentedWriter.writeLine(i < members.length - 1 ? ',' : '');
}
});
this._indentedWriter.write('}');
}
visitAstEnumValue(astEnumValue) {
this._writeAedocSynopsis(astEnumValue);
this._indentedWriter.write(astEnumValue.getDeclarationLine());
}
visitAstPackage(astPackage) {
for (const astItem of astPackage.getSortedMemberItems()) {
this.visit(astItem);
this._indentedWriter.writeLine();
this._indentedWriter.writeLine();
}
this._writeAedocSynopsis(astPackage);
}
visitAstNamespace(astNamespace) {
this._writeAedocSynopsis(astNamespace);
// We have decided to call the astNamespace a 'module' in our
// public API documentation.
this._indentedWriter.writeLine(`module ${astNamespace.name} {`);
this._indentedWriter.indentScope(() => {
for (const astItem of astNamespace.getSortedMemberItems()) {
this.visit(astItem);
this._indentedWriter.writeLine();
this._indentedWriter.writeLine();
}
});
this._indentedWriter.write('}');
}
visitAstModuleVariable(astModuleVariable) {
this._writeAedocSynopsis(astModuleVariable);
this._indentedWriter.write(`${astModuleVariable.name}: ${astModuleVariable.type} = ${astModuleVariable.value};`);
}
visitAstMember(astMember) {
if (astMember.documentation) {
this._writeAedocSynopsis(astMember);
}
this._indentedWriter.write(astMember.getDeclarationLine());
if (astMember.typeLiteral) {
this._insideTypeLiteral += 1;
this.visit(astMember.typeLiteral);
this._insideTypeLiteral -= 1;
}
}
visitAstFunction(astFunction) {
this._writeAedocSynopsis(astFunction);
this._indentedWriter.write(astFunction.getDeclarationLine());
}
/**
* Writes a synopsis of the AEDoc comments, which indicates the release tag,
* whether the item has been documented, and any warnings that were detected
* by the analysis.
*/
_writeAedocSynopsis(astItem) {
this._writeWarnings(astItem);
const lines = [];
if (astItem instanceof AstPackage_1.default && !astItem.documentation.summary.length) {
lines.push('(No packageDescription for this package)');
}
else {
let footer = '';
switch (astItem.documentation.releaseTag) {
case ReleaseTag_1.ReleaseTag.Internal:
footer += '@internal';
break;
case ReleaseTag_1.ReleaseTag.Alpha:
footer += '@alpha';
break;
case ReleaseTag_1.ReleaseTag.Beta:
footer += '@beta';
break;
case ReleaseTag_1.ReleaseTag.Public:
footer += '@public';
break;
}
// deprecatedMessage is initialized by default,
// this ensures it has contents before adding '@deprecated'
if (astItem.documentation.deprecatedMessage.length > 0) {
if (footer) {
footer += ' ';
}
footer += '@deprecated';
}
// If we are anywhere inside a TypeLiteral, _insideTypeLiteral is greater than 0
if (this._insideTypeLiteral === 0 && astItem.needsDocumentation) {
if (footer) {
footer += ' ';
}
footer += '(undocumented)';
}
if (footer) {
lines.push(footer);
}
}
this._writeLinesAsComments(lines);
}
_writeWarnings(astItem) {
const lines = astItem.warnings.map((x) => 'WARNING: ' + x);
this._writeLinesAsComments(lines);
}
_writeLinesAsComments(lines) {
if (lines.length) {
// Write the lines prefixed by slashes. If there are multiple lines, add "//" to each line
this._indentedWriter.write('// ');
this._indentedWriter.write(lines.join('\n// '));
this._indentedWriter.writeLine();
}
}
}
exports.default = ApiFileGenerator;
//# sourceMappingURL=ApiFileGenerator.js.map