UNPKG

@microsoft/api-extractor

Version:

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

203 lines (201 loc) 8.67 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 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