mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-12-12 03:20:56 -06:00
This adds a "small" d.ts bundler script. This script is very basic, using Node printing to produce its output. Generally speaking, this is inadvisable as it completely disregards name shadowing, globals, etc. However, in our case, we don't care about the globals, and we can opt to restructure our codebase in order to avoid conflict, which we largely had to do anyway when we were namespaces and everything was in scope.
414 lines
13 KiB
JavaScript
414 lines
13 KiB
JavaScript
/**
|
|
* WARNING: this is a very, very rudimentary d.ts bundler; it only works
|
|
* in the TS project thanks to our history using namespaces, which has
|
|
* prevented us from duplicating names across files, and allows us to
|
|
* bundle as namespaces again, even though the project is modules.
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import minimist from "minimist";
|
|
import url from "url";
|
|
import ts from "../lib/typescript.js";
|
|
import assert, { fail } from "assert";
|
|
|
|
const __filename = url.fileURLToPath(new URL(import.meta.url));
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
// /** @type {any} */ (ts).Debug.enableDebugInfo();
|
|
|
|
const dotDts = ".d.ts";
|
|
|
|
const options = minimist(process.argv.slice(2), {
|
|
string: ["project", "entrypoint", "output"],
|
|
});
|
|
|
|
const entrypoint = options.entrypoint;
|
|
const output = options.output;
|
|
|
|
assert(typeof entrypoint === "string" && entrypoint);
|
|
assert(typeof output === "string" && output);
|
|
assert(output.endsWith(dotDts));
|
|
|
|
const internalOutput = output.substring(0, output.length - dotDts.length) + ".internal" + dotDts;
|
|
|
|
console.log(`Bundling ${entrypoint} to ${output} and ${internalOutput}`);
|
|
|
|
const newLineKind = ts.NewLineKind.LineFeed;
|
|
const newLine = newLineKind === ts.NewLineKind.LineFeed ? "\n" : "\r\n";
|
|
|
|
/** @type {(node: ts.Node) => node is ts.DeclarationStatement} */
|
|
function isDeclarationStatement(node) {
|
|
return /** @type {any} */ (ts).isDeclarationStatement(node);
|
|
}
|
|
|
|
/** @type {(node: ts.Node) => boolean} */
|
|
function isInternalDeclaration(node) {
|
|
return /** @type {any} */ (ts).isInternalDeclaration(node, node.getSourceFile());
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {ts.VariableDeclaration} node
|
|
* @returns {ts.VariableStatement}
|
|
*/
|
|
function getParentVariableStatement(node) {
|
|
const declarationList = node.parent;
|
|
assert(ts.isVariableDeclarationList(declarationList), `expected VariableDeclarationList at ${nodeToLocation(node)}`);
|
|
assert(declarationList.declarations.length === 1, `expected VariableDeclarationList of length 1 at ${nodeToLocation(node)}`);
|
|
const variableStatement = declarationList.parent;
|
|
assert(ts.isVariableStatement(variableStatement), `expected VariableStatement at ${nodeToLocation(node)}`);
|
|
return variableStatement;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {ts.Declaration} node
|
|
* @returns {ts.Statement | undefined}
|
|
*/
|
|
function getDeclarationStatement(node) {
|
|
if (ts.isVariableDeclaration(node)) {
|
|
return getParentVariableStatement(node);
|
|
}
|
|
else if (isDeclarationStatement(node)) {
|
|
return node;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** @type {ts.TransformationContext} */
|
|
const nullTransformationContext = /** @type {any} */ (ts).nullTransformationContext;
|
|
|
|
const program = ts.createProgram([entrypoint], { target: ts.ScriptTarget.ES5 });
|
|
|
|
const typeChecker = program.getTypeChecker();
|
|
|
|
const sourceFile = program.getSourceFile(entrypoint);
|
|
assert(sourceFile, "Failed to load source file");
|
|
const moduleSymbol = typeChecker.getSymbolAtLocation(sourceFile);
|
|
assert(moduleSymbol, "Failed to get module's symbol");
|
|
|
|
const printer = ts.createPrinter({ newLine: newLineKind });
|
|
|
|
/** @type {string[]} */
|
|
const publicLines = [];
|
|
/** @type {string[]} */
|
|
const internalLines = [];
|
|
|
|
const indent = " ";
|
|
let currentIndent = "";
|
|
|
|
function increaseIndent() {
|
|
currentIndent += indent;
|
|
}
|
|
|
|
function decreaseIndent() {
|
|
currentIndent = currentIndent.slice(indent.length);
|
|
}
|
|
|
|
/**
|
|
* @enum {number}
|
|
*/
|
|
const WriteTarget = {
|
|
Public: 1 << 0,
|
|
Internal: 1 << 1,
|
|
Both: (1 << 0) | (1 << 1),
|
|
};
|
|
|
|
/**
|
|
* @param {string} s
|
|
* @param {WriteTarget} target
|
|
*/
|
|
function write(s, target) {
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
const toPush = !s ? [""] : s.split(/\r?\n/).filter(line => line).map(line => (currentIndent + line).trimEnd());
|
|
|
|
if (target & WriteTarget.Public) {
|
|
publicLines.push(...toPush);
|
|
}
|
|
if (target & WriteTarget.Internal) {
|
|
internalLines.push(...toPush);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Node} node
|
|
* @param {ts.SourceFile} sourceFile
|
|
* @param {WriteTarget} target
|
|
*/
|
|
function writeNode(node, sourceFile, target) {
|
|
write(printer.printNode(ts.EmitHint.Unspecified, node, sourceFile), target);
|
|
}
|
|
|
|
/** @type {Map<ts.Symbol, boolean>} */
|
|
const containsPublicAPICache = new Map();
|
|
|
|
/**
|
|
* @param {ts.Symbol} symbol
|
|
* @returns {boolean}
|
|
*/
|
|
function containsPublicAPI(symbol) {
|
|
const cached = containsPublicAPICache.get(symbol);
|
|
if (cached !== undefined) {
|
|
return cached;
|
|
}
|
|
|
|
const result = containsPublicAPIWorker();
|
|
containsPublicAPICache.set(symbol, result);
|
|
return result;
|
|
|
|
function containsPublicAPIWorker() {
|
|
if (!symbol.declarations?.length) {
|
|
return false;
|
|
}
|
|
|
|
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
const resolved = typeChecker.getAliasedSymbol(symbol);
|
|
return containsPublicAPI(resolved);
|
|
}
|
|
|
|
// Namespace barrel; actual namespaces are checked below.
|
|
if (symbol.flags & ts.SymbolFlags.ValueModule && symbol.valueDeclaration?.kind === ts.SyntaxKind.SourceFile) {
|
|
for (const me of typeChecker.getExportsOfModule(symbol)) {
|
|
if (containsPublicAPI(me)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
for (const decl of symbol.declarations) {
|
|
const statement = getDeclarationStatement(decl);
|
|
if (statement && !isInternalDeclaration(statement)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Node} node
|
|
*/
|
|
function nodeToLocation(node) {
|
|
const sourceFile = node.getSourceFile();
|
|
const lc = sourceFile.getLineAndCharacterOfPosition(node.pos);
|
|
return `${sourceFile.fileName}:${lc.line+1}:${lc.character+1}`;
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Node} node
|
|
* @returns {ts.Node | undefined}
|
|
*/
|
|
function removeDeclareConstExport(node) {
|
|
switch (node.kind) {
|
|
case ts.SyntaxKind.DeclareKeyword: // No need to emit this in d.ts files.
|
|
case ts.SyntaxKind.ConstKeyword: // Remove const from const enums.
|
|
case ts.SyntaxKind.ExportKeyword: // No export modifier; we are already in the namespace.
|
|
return undefined;
|
|
}
|
|
return node;
|
|
}
|
|
|
|
/** @type {Map<string, ts.Symbol>[]} */
|
|
const scopeStack = [];
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
function findInScope(name) {
|
|
for (let i = scopeStack.length-1; i >= 0; i--) {
|
|
const scope = scopeStack[i];
|
|
const symbol = scope.get(name);
|
|
if (symbol) {
|
|
return symbol;
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** @type {(symbol: ts.Symbol | undefined, excludes?: ts.SymbolFlags) => boolean} */
|
|
function isNonLocalAlias(symbol, excludes = ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace) {
|
|
if (!symbol) return false;
|
|
return (symbol.flags & (ts.SymbolFlags.Alias | excludes)) === ts.SymbolFlags.Alias || !!(symbol.flags & ts.SymbolFlags.Alias && symbol.flags & ts.SymbolFlags.Assignment);
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Symbol} symbol
|
|
*/
|
|
function resolveAlias(symbol) {
|
|
return typeChecker.getAliasedSymbol(symbol);
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Symbol} symbol
|
|
* @param {boolean | undefined} [dontResolveAlias]
|
|
*/
|
|
function resolveSymbol(symbol, dontResolveAlias = undefined) {
|
|
return !dontResolveAlias && isNonLocalAlias(symbol) ? resolveAlias(symbol) : symbol;
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Symbol} symbol
|
|
* @returns {ts.Symbol}
|
|
*/
|
|
function getMergedSymbol(symbol) {
|
|
return /** @type {any} */ (typeChecker).getMergedSymbol(symbol);
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Symbol} s1
|
|
* @param {ts.Symbol} s2
|
|
*/
|
|
function symbolsConflict(s1, s2) {
|
|
// See getSymbolIfSameReference in checker.ts
|
|
s1 = getMergedSymbol(resolveSymbol(getMergedSymbol(s1)));
|
|
s2 = getMergedSymbol(resolveSymbol(getMergedSymbol(s2)));
|
|
if (s1 === s2) {
|
|
return false;
|
|
}
|
|
|
|
const s1Flags = s1.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Value);
|
|
const s2Flags = s2.flags & (ts.SymbolFlags.Type | ts.SymbolFlags.Value);
|
|
|
|
// If the two symbols differ by type/value space, ignore.
|
|
if (!(s1Flags & s2Flags)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Node} node
|
|
* @returns {boolean}
|
|
*/
|
|
function isPartOfTypeNode(node) {
|
|
return /** @type {any} */ (ts).isPartOfTypeNode(node);
|
|
}
|
|
|
|
/**
|
|
* @param {ts.Statement} decl
|
|
*/
|
|
function verifyMatchingSymbols(decl) {
|
|
ts.visitEachChild(decl, /** @type {(node: ts.Node) => ts.Node} */ function visit(node) {
|
|
if (ts.isIdentifier(node) && isPartOfTypeNode(node)) {
|
|
if (ts.isQualifiedName(node.parent) && node !== node.parent.left) {
|
|
return node;
|
|
}
|
|
if (ts.isParameter(node.parent) && node === node.parent.name) {
|
|
return node;
|
|
}
|
|
if (ts.isNamedTupleMember(node.parent) && node === node.parent.name) {
|
|
return node;
|
|
}
|
|
|
|
const symbolOfNode = typeChecker.getSymbolAtLocation(node);
|
|
if (!symbolOfNode) {
|
|
fail(`No symbol for node at ${nodeToLocation(node)}`);
|
|
}
|
|
const symbolInScope = findInScope(symbolOfNode.name);
|
|
if (!symbolInScope) {
|
|
// We didn't find the symbol in scope at all. Just allow it and we'll fail at test time.
|
|
return node;
|
|
}
|
|
|
|
if (symbolsConflict(symbolOfNode, symbolInScope)) {
|
|
fail(`Declaration at ${nodeToLocation(decl)}\n references ${symbolOfNode.name} at ${symbolOfNode.declarations && nodeToLocation(symbolOfNode.declarations[0])},\n but containing scope contains a symbol with the same name declared at ${symbolInScope.declarations && nodeToLocation(symbolInScope.declarations[0])}`);
|
|
}
|
|
}
|
|
|
|
return ts.visitEachChild(node, visit, nullTransformationContext);
|
|
}, nullTransformationContext);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
* @param {ts.Symbol} moduleSymbol
|
|
*/
|
|
function emitAsNamespace(name, moduleSymbol) {
|
|
assert(moduleSymbol.flags & ts.SymbolFlags.ValueModule, "moduleSymbol is not a module");
|
|
|
|
scopeStack.push(new Map());
|
|
const currentScope = scopeStack[scopeStack.length-1];
|
|
|
|
const target = containsPublicAPI(moduleSymbol) ? WriteTarget.Both : WriteTarget.Internal;
|
|
|
|
if (name === "ts") {
|
|
// We will write `export = ts` at the end.
|
|
write(`declare namespace ${name} {`, target);
|
|
}
|
|
else {
|
|
// No export modifier; we are already in the namespace.
|
|
write(`namespace ${name} {`, target);
|
|
}
|
|
increaseIndent();
|
|
|
|
const moduleExports = typeChecker.getExportsOfModule(moduleSymbol);
|
|
for (const me of moduleExports) {
|
|
currentScope.set(me.name, me);
|
|
}
|
|
|
|
for (const me of moduleExports) {
|
|
assert(me.declarations?.length);
|
|
|
|
if (me.flags & ts.SymbolFlags.Alias) {
|
|
const resolved = typeChecker.getAliasedSymbol(me);
|
|
emitAsNamespace(me.name, resolved);
|
|
continue;
|
|
}
|
|
|
|
for (const decl of me.declarations) {
|
|
const statement = getDeclarationStatement(decl);
|
|
const sourceFile = decl.getSourceFile();
|
|
|
|
if (!statement) {
|
|
fail(`Unhandled declaration for ${me.name} at ${nodeToLocation(decl)}`);
|
|
}
|
|
|
|
verifyMatchingSymbols(statement);
|
|
|
|
const isInternal = isInternalDeclaration(statement);
|
|
if (!isInternal) {
|
|
const publicStatement = ts.visitEachChild(statement, (node) => {
|
|
// No @internal comments in the public API.
|
|
if (isInternalDeclaration(node)) {
|
|
return undefined;
|
|
}
|
|
return removeDeclareConstExport(node);
|
|
}, nullTransformationContext);
|
|
|
|
writeNode(publicStatement, sourceFile, WriteTarget.Public);
|
|
}
|
|
|
|
const internalStatement = ts.visitEachChild(statement, removeDeclareConstExport, nullTransformationContext);
|
|
|
|
writeNode(internalStatement, sourceFile, WriteTarget.Internal);
|
|
}
|
|
}
|
|
|
|
scopeStack.pop();
|
|
|
|
decreaseIndent();
|
|
write(`}`, target);
|
|
}
|
|
|
|
emitAsNamespace("ts", moduleSymbol);
|
|
|
|
write("export = ts;", WriteTarget.Both);
|
|
|
|
const copyrightNotice = fs.readFileSync(path.join(__dirname, "..", "CopyrightNotice.txt"), "utf-8");
|
|
const publicContents = copyrightNotice + publicLines.join(newLine);
|
|
const internalContents = copyrightNotice + internalLines.join(newLine);
|
|
|
|
if (publicContents.includes("@internal")) {
|
|
console.error("Output includes untrimmed @internal nodes!");
|
|
}
|
|
|
|
fs.writeFileSync(output, publicContents);
|
|
fs.writeFileSync(internalOutput, internalContents);
|