4487f82c创建于 2025年6月3日历史提交
/**

 * 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";



/**

 *

 * @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 (ts.isDeclarationStatement(node)) {

        return node;

    }

    return undefined;

}



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 && !ts.isInternalDeclaration(statement, statement.getSourceFile())) {

                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

 * @param {boolean | undefined} [dontResolveAlias]

 */

function resolveSymbol(symbol, dontResolveAlias = undefined) {

    return !dontResolveAlias && isNonLocalAlias(symbol) ? typeChecker.getAliasedSymbol(symbol) : symbol;

}



/**

 * @param {ts.Symbol} symbol

 * @returns {ts.Symbol}

 */

function getMergedSymbol(symbol) {

    return 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.Statement} decl

 */

function verifyMatchingSymbols(decl) {

    ts.visitEachChild(decl, /** @type {(node: ts.Node) => ts.Node} */ function visit(node) {

        if (ts.isIdentifier(node) && ts.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, ts.nullTransformationContext);

    }, ts.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 = ts.isInternalDeclaration(statement, statement.getSourceFile());

            if (!isInternal) {

                const publicStatement = ts.visitEachChild(statement, (node) => {

                    // No @internal comments in the public API.

                    if (ts.isInternalDeclaration(node, node.getSourceFile())) {

                        return undefined;

                    }

                    return removeDeclareConstExport(node);

                }, ts.nullTransformationContext);



                writeNode(publicStatement, sourceFile, WriteTarget.Public);

            }



            const internalStatement = ts.visitEachChild(statement, removeDeclareConstExport, ts.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);