UNPKG

@microsoft/api-extractor

Version:

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

437 lines 22.5 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 }); /* tslint:disable:no-bitwise */ const ts = require("typescript"); const tsdoc = require("@microsoft/tsdoc"); const node_core_library_1 = require("@microsoft/node-core-library"); const IndentedWriter_1 = require("../../utils/IndentedWriter"); const TypeScriptHelpers_1 = require("../../utils/TypeScriptHelpers"); const Span_1 = require("../../utils/Span"); const ReleaseTag_1 = require("../../aedoc/ReleaseTag"); const AstSymbolTable_1 = require("./AstSymbolTable"); const DtsEntry_1 = require("./DtsEntry"); const SymbolAnalyzer_1 = require("./SymbolAnalyzer"); /** * Used with DtsRollupGenerator.writeTypingsFile() */ var DtsRollupKind; (function (DtsRollupKind) { /** * Generate a *.d.ts file for an internal release, or for the trimming=false mode. * This output file will contain all definitions that are reachable from the entry point. */ DtsRollupKind[DtsRollupKind["InternalRelease"] = 0] = "InternalRelease"; /** * Generate a *.d.ts file for a preview release. * This output file will contain all definitions that are reachable from the entry point, * except definitions marked as \@alpha or \@internal. */ DtsRollupKind[DtsRollupKind["BetaRelease"] = 1] = "BetaRelease"; /** * Generate a *.d.ts file for a public release. * This output file will contain all definitions that are reachable from the entry point, * except definitions marked as \@beta, \@alpha, or \@internal. */ DtsRollupKind[DtsRollupKind["PublicRelease"] = 2] = "PublicRelease"; })(DtsRollupKind = exports.DtsRollupKind || (exports.DtsRollupKind = {})); class DtsRollupGenerator { constructor(context) { this._dtsEntries = []; this._dtsEntriesByAstSymbol = new Map(); this._dtsEntriesBySymbol = new Map(); this._releaseTagByAstSymbol = new Map(); /** * A list of names (e.g. "example-library") that should appear in a reference like this: * * /// <reference types="example-library" /> */ this._dtsTypeDefinitionReferences = []; this._context = context; this._typeChecker = context.typeChecker; this._tsdocParser = new tsdoc.TSDocParser(); this._astSymbolTable = new AstSymbolTable_1.AstSymbolTable(this._context.typeChecker, this._context.packageJsonLookup); } /** * Perform the analysis. This must be called before writeTypingsFile(). */ analyze() { if (this._astEntryPoint) { throw new Error('DtsRollupGenerator.analyze() was already called'); } // Build the entry point const sourceFile = this._context.package.getDeclaration().getSourceFile(); this._astEntryPoint = this._astSymbolTable.fetchEntryPoint(sourceFile); const exportedAstSymbols = []; // Create a DtsEntry for each top-level export for (const exportedMember of this._astEntryPoint.exportedMembers) { const astSymbol = exportedMember.astSymbol; this._createDtsEntryForSymbol(exportedMember.astSymbol, exportedMember.name); exportedAstSymbols.push(astSymbol); } // Create a DtsEntry for each indirectly referenced export. // Note that we do this *after* the above loop, so that references to exported AstSymbols // are encountered first as exports. const alreadySeenAstSymbols = new Set(); for (const exportedAstSymbol of exportedAstSymbols) { this._createDtsEntryForIndirectReferences(exportedAstSymbol, alreadySeenAstSymbols); } this._makeUniqueNames(); this._dtsEntries.sort((a, b) => a.getSortKey().localeCompare(b.getSortKey())); this._dtsTypeDefinitionReferences.sort(); } /** * Generates the typings file and writes it to disk. * * @param dtsFilename - The *.d.ts output filename */ writeTypingsFile(dtsFilename, dtsKind) { const indentedWriter = new IndentedWriter_1.IndentedWriter(); this._generateTypingsFileContent(indentedWriter, dtsKind); node_core_library_1.FileSystem.writeFile(dtsFilename, indentedWriter.toString(), { convertLineEndings: "\r\n" /* CrLf */, ensureFolderExists: true }); } get astEntryPoint() { if (!this._astEntryPoint) { throw new Error('DtsRollupGenerator.analyze() was not called'); } return this._astEntryPoint; } _createDtsEntryForSymbol(astSymbol, exportedName) { let dtsEntry = this._dtsEntriesByAstSymbol.get(astSymbol); if (!dtsEntry) { dtsEntry = new DtsEntry_1.DtsEntry({ astSymbol: astSymbol, originalName: exportedName || astSymbol.localName, exported: !!exportedName }); this._dtsEntriesByAstSymbol.set(astSymbol, dtsEntry); this._dtsEntriesBySymbol.set(astSymbol.followedSymbol, dtsEntry); this._dtsEntries.push(dtsEntry); this._collectTypeDefinitionReferences(astSymbol); } else { if (exportedName) { if (!dtsEntry.exported) { throw new Error('Program Bug: DtsEntry should have been marked as exported'); } if (dtsEntry.originalName !== exportedName) { throw new Error(`The symbol ${exportedName} was also exported as ${dtsEntry.originalName};` + ` this is not supported yet`); } } } } _createDtsEntryForIndirectReferences(astSymbol, alreadySeenAstSymbols) { if (alreadySeenAstSymbols.has(astSymbol)) { return; } alreadySeenAstSymbols.add(astSymbol); astSymbol.forEachDeclarationRecursive((astDeclaration) => { for (const referencedAstSymbol of astDeclaration.referencedAstSymbols) { this._createDtsEntryForSymbol(referencedAstSymbol, undefined); this._createDtsEntryForIndirectReferences(referencedAstSymbol, alreadySeenAstSymbols); } }); } /** * Ensures a unique name for each item in the package typings file. */ _makeUniqueNames() { const usedNames = new Set(); // First collect the explicit package exports for (const dtsEntry of this._dtsEntries) { if (dtsEntry.exported) { if (usedNames.has(dtsEntry.originalName)) { // This should be impossible throw new Error(`Program bug: a package cannot have two exports with the name ${dtsEntry.originalName}`); } dtsEntry.nameForEmit = dtsEntry.originalName; usedNames.add(dtsEntry.nameForEmit); } } // Next generate unique names for the non-exports that will be emitted for (const dtsEntry of this._dtsEntries) { if (!dtsEntry.exported) { let suffix = 1; dtsEntry.nameForEmit = dtsEntry.originalName; while (usedNames.has(dtsEntry.nameForEmit)) { dtsEntry.nameForEmit = `${dtsEntry.originalName}_${++suffix}`; } usedNames.add(dtsEntry.nameForEmit); } } } _generateTypingsFileContent(indentedWriter, dtsKind) { indentedWriter.spacing = ''; indentedWriter.clear(); // If there is a @packagedocumentation header, put it first: const packageDocumentation = this._context.package.documentation.emitNormalizedComment(); if (packageDocumentation) { indentedWriter.writeLine(packageDocumentation); indentedWriter.writeLine(); } // Emit the triple slash directives for (const typeDirectiveReference of this._dtsTypeDefinitionReferences) { // tslint:disable-next-line:max-line-length // https://github.com/Microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 indentedWriter.writeLine(`/// <reference types="${typeDirectiveReference}" />`); } // Emit the imports for (const dtsEntry of this._dtsEntries) { if (dtsEntry.astSymbol.astImport) { const releaseTag = this._getReleaseTagForAstSymbol(dtsEntry.astSymbol); if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { const astImport = dtsEntry.astSymbol.astImport; if (astImport.exportName === '*') { indentedWriter.write(`import * as ${dtsEntry.nameForEmit}`); } else if (dtsEntry.nameForEmit !== astImport.exportName) { indentedWriter.write(`import { ${astImport.exportName} as ${dtsEntry.nameForEmit} }`); } else { indentedWriter.write(`import { ${astImport.exportName} }`); } indentedWriter.writeLine(` from '${astImport.modulePath}';`); } } } // Emit the regular declarations for (const dtsEntry of this._dtsEntries) { if (!dtsEntry.astSymbol.astImport) { const releaseTag = this._getReleaseTagForAstSymbol(dtsEntry.astSymbol); if (this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { // Emit all the declarations for this entry for (const astDeclaration of dtsEntry.astSymbol.astDeclarations || []) { indentedWriter.writeLine(); const span = new Span_1.Span(astDeclaration.declaration); this._modifySpan(span, dtsEntry, astDeclaration, dtsKind); indentedWriter.writeLine(span.getModifiedText()); } } else { indentedWriter.writeLine(); indentedWriter.writeLine(`/* Excluded from this release type: ${dtsEntry.nameForEmit} */`); } } } } /** * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. */ _modifySpan(span, dtsEntry, astDeclaration, dtsKind) { const previousSpan = span.previousSibling; let recurseChildren = true; switch (span.kind) { case ts.SyntaxKind.JSDocComment: // If the @packagedocumentation comment seems to be attached to one of the regular API items, // omit it. It gets explictly emitted at the top of the file. if (span.node.getText().match(/(?:\s|\*)@packagedocumentation(?:\s|\*)/g)) { span.modification.skipAll(); } // For now, we don't transform JSDoc comment nodes at all recurseChildren = false; break; case ts.SyntaxKind.ExportKeyword: case ts.SyntaxKind.DefaultKeyword: case ts.SyntaxKind.DeclareKeyword: // Delete any explicit "export" or "declare" keywords -- we will re-add them below span.modification.skipAll(); break; case ts.SyntaxKind.InterfaceKeyword: case ts.SyntaxKind.ClassKeyword: case ts.SyntaxKind.EnumKeyword: case ts.SyntaxKind.NamespaceKeyword: case ts.SyntaxKind.ModuleKeyword: case ts.SyntaxKind.TypeKeyword: case ts.SyntaxKind.FunctionKeyword: // Replace the stuff we possibly deleted above let replacedModifiers = ''; // Add a declare statement for root declarations (but not for nested declarations) if (!astDeclaration.parent) { replacedModifiers += 'declare '; } if (dtsEntry.exported) { replacedModifiers = 'export ' + replacedModifiers; } if (previousSpan && previousSpan.kind === ts.SyntaxKind.SyntaxList) { // If there is a previous span of type SyntaxList, then apply it before any other modifiers // (e.g. "abstract") that appear there. previousSpan.modification.prefix = replacedModifiers + previousSpan.modification.prefix; } else { // Otherwise just stick it in front of this span span.modification.prefix = replacedModifiers + span.modification.prefix; } break; case ts.SyntaxKind.VariableDeclaration: if (!span.parent) { // The VariableDeclaration node is part of a VariableDeclarationList, however // the Entry.followedSymbol points to the VariableDeclaration part because // multiple definitions might share the same VariableDeclarationList. // // Since we are emitting a separate declaration for each one, we need to look upwards // in the ts.Node tree and write a copy of the enclosing VariableDeclarationList // content (e.g. "var" from "var x=1, y=2"). const list = TypeScriptHelpers_1.TypeScriptHelpers.matchAncestor(span.node, [ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.VariableDeclaration]); if (!list) { throw new Error('Unsupported variable declaration'); } const listPrefix = list.getSourceFile().text .substring(list.getStart(), list.declarations[0].getStart()); span.modification.prefix = 'declare ' + listPrefix + span.modification.prefix; span.modification.suffix = ';'; } break; case ts.SyntaxKind.Identifier: let nameFixup = false; const identifierSymbol = this._typeChecker.getSymbolAtLocation(span.node); if (identifierSymbol) { const followedSymbol = TypeScriptHelpers_1.TypeScriptHelpers.followAliases(identifierSymbol, this._typeChecker); const referencedDtsEntry = this._dtsEntriesBySymbol.get(followedSymbol); if (referencedDtsEntry) { if (!referencedDtsEntry.nameForEmit) { // This should never happen throw new Error('referencedEntry.uniqueName is undefined'); } span.modification.prefix = referencedDtsEntry.nameForEmit; nameFixup = true; // For debugging: // span.modification.prefix += '/*R=FIX*/'; } } if (!nameFixup) { // For debugging: // span.modification.prefix += '/*R=KEEP*/'; } break; } if (recurseChildren) { for (const child of span.children) { let childAstDeclaration = astDeclaration; // Should we trim this node? let trimmed = false; if (SymbolAnalyzer_1.SymbolAnalyzer.isAstDeclaration(child.kind)) { childAstDeclaration = this._astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); const releaseTag = this._getReleaseTagForAstSymbol(childAstDeclaration.astSymbol); if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { const modification = child.modification; // Yes, trim it and stop here const name = childAstDeclaration.astSymbol.localName; modification.omitChildren = true; modification.prefix = `/* Excluded from this release type: ${name} */`; modification.suffix = ''; if (child.children.length > 0) { // If there are grandchildren, then keep the last grandchild's separator, // since it often has useful whitespace modification.suffix = child.children[child.children.length - 1].separator; } if (child.nextSibling) { // If the thing we are trimming is followed by a comma, then trim the comma also. // An example would be an enum member. if (child.nextSibling.kind === ts.SyntaxKind.CommaToken) { // Keep its separator since it often has useful whitespace modification.suffix += child.nextSibling.separator; child.nextSibling.modification.skipAll(); } } trimmed = true; } } if (!trimmed) { this._modifySpan(child, dtsEntry, childAstDeclaration, dtsKind); } } } } _shouldIncludeReleaseTag(releaseTag, dtsKind) { switch (dtsKind) { case DtsRollupKind.InternalRelease: return true; case DtsRollupKind.BetaRelease: // NOTE: If the release tag is "None", then we don't have enough information to trim it return releaseTag === ReleaseTag_1.ReleaseTag.Beta || releaseTag === ReleaseTag_1.ReleaseTag.Public || releaseTag === ReleaseTag_1.ReleaseTag.None; case DtsRollupKind.PublicRelease: return releaseTag === ReleaseTag_1.ReleaseTag.Public || releaseTag === ReleaseTag_1.ReleaseTag.None; } throw new Error(`DtsRollupKind[dtsKind] is not implemented`); } _getReleaseTagForAstSymbol(astSymbol) { let releaseTag = this._releaseTagByAstSymbol.get(astSymbol); if (releaseTag) { return releaseTag; } releaseTag = ReleaseTag_1.ReleaseTag.None; let current = astSymbol; while (current) { for (const astDeclaration of current.astDeclarations) { const declarationReleaseTag = this._getReleaseTagForDeclaration(astDeclaration.declaration); if (releaseTag !== ReleaseTag_1.ReleaseTag.None && declarationReleaseTag !== releaseTag) { // this._analyzeWarnings.push('WARNING: Conflicting release tags found for ' + symbol.name); break; } releaseTag = declarationReleaseTag; } if (releaseTag !== ReleaseTag_1.ReleaseTag.None) { break; } current = current.parentAstSymbol; } if (releaseTag === ReleaseTag_1.ReleaseTag.None) { releaseTag = ReleaseTag_1.ReleaseTag.Public; // public by default } this._releaseTagByAstSymbol.set(astSymbol, releaseTag); return releaseTag; } // NOTE: THIS IS A TEMPORARY WORKAROUND. // In the near future we will overhaul the AEDoc parser to separate syntactic/semantic analysis, // at which point this will be wired up to the same ApiDocumentation layer used for the API Review files _getReleaseTagForDeclaration(declaration) { const sourceFileText = declaration.getSourceFile().text; for (const commentRange of TypeScriptHelpers_1.TypeScriptHelpers.getJSDocCommentRanges(declaration, sourceFileText) || []) { // NOTE: This string includes "/**" const commentTextRange = tsdoc.TextRange.fromStringRange(sourceFileText, commentRange.pos, commentRange.end); const parserContext = this._tsdocParser.parseRange(commentTextRange); const modifierTagSet = parserContext.docComment.modifierTagSet; if (modifierTagSet.isPublic()) { return ReleaseTag_1.ReleaseTag.Public; } if (modifierTagSet.isBeta()) { return ReleaseTag_1.ReleaseTag.Beta; } if (modifierTagSet.isAlpha()) { return ReleaseTag_1.ReleaseTag.Alpha; } if (modifierTagSet.isInternal()) { return ReleaseTag_1.ReleaseTag.Internal; } } return ReleaseTag_1.ReleaseTag.None; } _collectTypeDefinitionReferences(astSymbol) { // Are we emitting declarations? if (astSymbol.astImport) { return; // no, it's an import } const seenFilenames = new Set(); for (const astDeclaration of astSymbol.astDeclarations) { const sourceFile = astDeclaration.declaration.getSourceFile(); if (sourceFile && sourceFile.fileName) { if (!seenFilenames.has(sourceFile.fileName)) { seenFilenames.add(sourceFile.fileName); for (const typeReferenceDirective of sourceFile.typeReferenceDirectives) { const name = sourceFile.text.substring(typeReferenceDirective.pos, typeReferenceDirective.end); if (this._dtsTypeDefinitionReferences.indexOf(name) < 0) { this._dtsTypeDefinitionReferences.push(name); } } } } } } } exports.DtsRollupGenerator = DtsRollupGenerator; //# sourceMappingURL=DtsRollupGenerator.js.map