@microsoft/api-extractor
Version:
Validate, document, and review the exported API for a TypeScript library
437 lines • 22.5 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 });
/* 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|\*)/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