UNPKG

@microsoft/api-extractor

Version:

Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups

422 lines 23.7 kB
"use strict"; // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.DtsRollupGenerator = exports.DtsRollupKind = void 0; /* eslint-disable no-bitwise */ const ts = __importStar(require("typescript")); const node_core_library_1 = require("@rushstack/node-core-library"); const api_extractor_model_1 = require("@microsoft/api-extractor-model"); const TypeScriptHelpers_1 = require("../analyzer/TypeScriptHelpers"); const Span_1 = require("../analyzer/Span"); const AstImport_1 = require("../analyzer/AstImport"); const AstDeclaration_1 = require("../analyzer/AstDeclaration"); const AstSymbol_1 = require("../analyzer/AstSymbol"); const IndentedWriter_1 = require("./IndentedWriter"); const DtsEmitHelpers_1 = require("./DtsEmitHelpers"); const AstNamespaceImport_1 = require("../analyzer/AstNamespaceImport"); const SourceFileLocationFormatter_1 = require("../analyzer/SourceFileLocationFormatter"); /** * 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 \@internal. */ DtsRollupKind[DtsRollupKind["AlphaRelease"] = 1] = "AlphaRelease"; /** * 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"] = 2] = "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"] = 3] = "PublicRelease"; })(DtsRollupKind || (exports.DtsRollupKind = DtsRollupKind = {})); class DtsRollupGenerator { /** * Generates the typings file and writes it to disk. * * @param dtsFilename - The *.d.ts output filename */ static writeTypingsFile(collector, dtsFilename, dtsKind, newlineKind) { const writer = new IndentedWriter_1.IndentedWriter(); writer.trimLeadingSpaces = true; DtsRollupGenerator._generateTypingsFileContent(collector, writer, dtsKind); node_core_library_1.FileSystem.writeFile(dtsFilename, writer.toString(), { convertLineEndings: newlineKind, ensureFolderExists: true }); } static _generateTypingsFileContent(collector, writer, dtsKind) { // Emit the @packageDocumentation comment at the top of the file if (collector.workingPackage.tsdocParserContext) { writer.trimLeadingSpaces = false; writer.writeLine(collector.workingPackage.tsdocParserContext.sourceRange.toString()); writer.trimLeadingSpaces = true; writer.ensureSkippedLine(); } // Emit the triple slash directives for (const typeDirectiveReference of collector.dtsTypeReferenceDirectives) { // https://github.com/microsoft/TypeScript/blob/611ebc7aadd7a44a4c0447698bfda9222a78cb66/src/compiler/declarationEmitter.ts#L162 writer.writeLine(`/// <reference types="${typeDirectiveReference}" />`); } for (const libDirectiveReference of collector.dtsLibReferenceDirectives) { writer.writeLine(`/// <reference lib="${libDirectiveReference}" />`); } writer.ensureSkippedLine(); // Emit the imports for (const entity of collector.entities) { if (entity.astEntity instanceof AstImport_1.AstImport) { // Note: it isn't valid to trim imports based on their release tags. // E.g. class Foo (`@public`) extends interface Bar (`@beta`) from some external library. // API-Extractor cannot trim `import { Bar } from "external-library"` when generating its public rollup, // or the export of `Foo` would include a broken reference to `Bar`. const astImport = entity.astEntity; DtsEmitHelpers_1.DtsEmitHelpers.emitImport(writer, entity, astImport); } } writer.ensureSkippedLine(); // Emit the regular declarations for (const entity of collector.entities) { const astEntity = entity.astEntity; const symbolMetadata = collector.tryFetchMetadataForAstEntity(astEntity); const maxEffectiveReleaseTag = symbolMetadata ? symbolMetadata.maxEffectiveReleaseTag : api_extractor_model_1.ReleaseTag.None; if (!this._shouldIncludeReleaseTag(maxEffectiveReleaseTag, dtsKind)) { if (!collector.extractorConfig.omitTrimmingComments) { writer.ensureSkippedLine(); writer.writeLine(`/* Excluded from this release type: ${entity.nameForEmit} */`); } continue; } if (astEntity instanceof AstSymbol_1.AstSymbol) { // Emit all the declarations for this entry for (const astDeclaration of astEntity.astDeclarations || []) { const apiItemMetadata = collector.fetchApiItemMetadata(astDeclaration); if (!this._shouldIncludeReleaseTag(apiItemMetadata.effectiveReleaseTag, dtsKind)) { if (!collector.extractorConfig.omitTrimmingComments) { writer.ensureSkippedLine(); writer.writeLine(`/* Excluded declaration from this release type: ${entity.nameForEmit} */`); } continue; } else { const span = new Span_1.Span(astDeclaration.declaration); DtsRollupGenerator._modifySpan(collector, span, entity, astDeclaration, dtsKind); writer.ensureSkippedLine(); span.writeModifiedText(writer); writer.ensureNewLine(); } } } if (astEntity instanceof AstNamespaceImport_1.AstNamespaceImport) { const astModuleExportInfo = astEntity.fetchAstModuleExportInfo(collector); if (entity.nameForEmit === undefined) { // This should never happen throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined'); } if (astModuleExportInfo.starExportedExternalModules.size > 0) { // We could support this, but we would need to find a way to safely represent it. throw new Error(`The ${entity.nameForEmit} namespace import includes a start export, which is not supported:\n` + SourceFileLocationFormatter_1.SourceFileLocationFormatter.formatDeclaration(astEntity.declaration)); } // Emit a synthetic declaration for the namespace. It will look like this: // // declare namespace example { // export { // f1, // f2 // } // } // // Note that we do not try to relocate f1()/f2() to be inside the namespace because other type // signatures may reference them directly (without using the namespace qualifier). writer.ensureSkippedLine(); if (entity.shouldInlineExport) { writer.write('export '); } writer.writeLine(`declare namespace ${entity.nameForEmit} {`); // all local exports of local imported module are just references to top-level declarations writer.increaseIndent(); writer.writeLine('export {'); writer.increaseIndent(); const exportClauses = []; for (const [exportedName, exportedEntity] of astModuleExportInfo.exportedLocalEntities) { const collectorEntity = collector.tryGetCollectorEntity(exportedEntity); if (collectorEntity === undefined) { // This should never happen // top-level exports of local imported module should be added as collector entities before throw new node_core_library_1.InternalError(`Cannot find collector entity for ${entity.nameForEmit}.${exportedEntity.localName}`); } // If the entity's declaration won't be included, then neither should the namespace export it // This fixes the issue encountered here: https://github.com/microsoft/rushstack/issues/2791 const exportedSymbolMetadata = collector.tryFetchMetadataForAstEntity(exportedEntity); const exportedMaxEffectiveReleaseTag = exportedSymbolMetadata ? exportedSymbolMetadata.maxEffectiveReleaseTag : api_extractor_model_1.ReleaseTag.None; if (!this._shouldIncludeReleaseTag(exportedMaxEffectiveReleaseTag, dtsKind)) { continue; } if (collectorEntity.nameForEmit === exportedName) { exportClauses.push(collectorEntity.nameForEmit); } else { exportClauses.push(`${collectorEntity.nameForEmit} as ${exportedName}`); } } writer.writeLine(exportClauses.join(',\n')); writer.decreaseIndent(); writer.writeLine('}'); // end of "export { ... }" writer.decreaseIndent(); writer.writeLine('}'); // end of "declare namespace { ... }" } if (!entity.shouldInlineExport) { for (const exportName of entity.exportNames) { DtsEmitHelpers_1.DtsEmitHelpers.emitNamedExport(writer, exportName, entity); } } writer.ensureSkippedLine(); } DtsEmitHelpers_1.DtsEmitHelpers.emitStarExports(writer, collector); // Emit "export { }" which is a special directive that prevents consumers from importing declarations // that don't have an explicit "export" modifier. writer.ensureSkippedLine(); writer.writeLine('export { }'); } /** * Before writing out a declaration, _modifySpan() applies various fixups to make it nice. */ static _modifySpan(collector, span, entity, 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|\*)/gi)) { 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 (entity.shouldInlineExport) { 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: // Is this a top-level variable declaration? // (The logic below does not apply to variable declarations that are part of an explicit "namespace" block, // since the compiler prefers not to emit "declare" or "export" keywords for those declarations.) 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) { // This should not happen unless the compiler API changes somehow throw new node_core_library_1.InternalError('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 = ';'; if (entity.shouldInlineExport) { span.modification.prefix = 'export ' + span.modification.prefix; } const declarationMetadata = collector.fetchDeclarationMetadata(astDeclaration); if (declarationMetadata.tsdocParserContext) { // Typically the comment for a variable declaration is attached to the outer variable statement // (which may possibly contain multiple variable declarations), so it's not part of the Span. // Instead we need to manually inject it. let originalComment = declarationMetadata.tsdocParserContext.sourceRange.toString(); if (!/\r?\n\s*$/.test(originalComment)) { originalComment += '\n'; } span.modification.indentDocComment = Span_1.IndentDocCommentScope.PrefixOnly; span.modification.prefix = originalComment + span.modification.prefix; } } break; case ts.SyntaxKind.Identifier: { const referencedEntity = collector.tryGetEntityForNode(span.node); if (referencedEntity) { if (!referencedEntity.nameForEmit) { // This should never happen throw new node_core_library_1.InternalError('referencedEntry.nameForEmit is undefined'); } span.modification.prefix = referencedEntity.nameForEmit; // For debugging: // span.modification.prefix += '/*R=FIX*/'; } else { // For debugging: // span.modification.prefix += '/*R=KEEP*/'; } } break; case ts.SyntaxKind.ImportType: DtsEmitHelpers_1.DtsEmitHelpers.modifyImportTypeSpan(collector, span, astDeclaration, (childSpan, childAstDeclaration) => { DtsRollupGenerator._modifySpan(collector, childSpan, entity, childAstDeclaration, dtsKind); }); break; } if (recurseChildren) { for (const child of span.children) { let childAstDeclaration = astDeclaration; // Should we trim this node? let trimmed = false; if (AstDeclaration_1.AstDeclaration.isSupportedSyntaxKind(child.kind)) { childAstDeclaration = collector.astSymbolTable.getChildAstDeclarationByNode(child.node, astDeclaration); const releaseTag = collector.fetchApiItemMetadata(childAstDeclaration).effectiveReleaseTag; if (!this._shouldIncludeReleaseTag(releaseTag, dtsKind)) { let nodeToTrim = child; // If we are trimming a variable statement, then we need to trim the outer VariableDeclarationList // as well. if (child.kind === ts.SyntaxKind.VariableDeclaration) { const variableStatement = child.findFirstParent(ts.SyntaxKind.VariableStatement); if (variableStatement !== undefined) { nodeToTrim = variableStatement; } } const modification = nodeToTrim.modification; // Yes, trim it and stop here const name = childAstDeclaration.astSymbol.localName; modification.omitChildren = true; if (!collector.extractorConfig.omitTrimmingComments) { modification.prefix = `/* Excluded from this release type: ${name} */`; } else { modification.prefix = ''; } modification.suffix = ''; if (nodeToTrim.children.length > 0) { // If there are grandchildren, then keep the last grandchild's separator, // since it often has useful whitespace modification.suffix = nodeToTrim.children[nodeToTrim.children.length - 1].separator; } if (nodeToTrim.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 (nodeToTrim.nextSibling.kind === ts.SyntaxKind.CommaToken) { // Keep its separator since it often has useful whitespace modification.suffix += nodeToTrim.nextSibling.separator; nodeToTrim.nextSibling.modification.skipAll(); } } trimmed = true; } } if (!trimmed) { DtsRollupGenerator._modifySpan(collector, child, entity, childAstDeclaration, dtsKind); } } } } static _shouldIncludeReleaseTag(releaseTag, dtsKind) { switch (dtsKind) { case DtsRollupKind.InternalRelease: return true; case DtsRollupKind.AlphaRelease: return (releaseTag === api_extractor_model_1.ReleaseTag.Alpha || releaseTag === api_extractor_model_1.ReleaseTag.Beta || releaseTag === api_extractor_model_1.ReleaseTag.Public || // NOTE: If the release tag is "None", then we don't have enough information to trim it releaseTag === api_extractor_model_1.ReleaseTag.None); case DtsRollupKind.BetaRelease: return (releaseTag === api_extractor_model_1.ReleaseTag.Beta || releaseTag === api_extractor_model_1.ReleaseTag.Public || // NOTE: If the release tag is "None", then we don't have enough information to trim it releaseTag === api_extractor_model_1.ReleaseTag.None); case DtsRollupKind.PublicRelease: return releaseTag === api_extractor_model_1.ReleaseTag.Public || releaseTag === api_extractor_model_1.ReleaseTag.None; default: throw new Error(`${DtsRollupKind[dtsKind]} is not implemented`); } } } exports.DtsRollupGenerator = DtsRollupGenerator; //# sourceMappingURL=DtsRollupGenerator.js.map