mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Support split CommonJS require bindings (#1945)
This commit is contained in:
@@ -28,8 +28,10 @@ import {
|
||||
isModuleExportsAccess,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
requireBindingEarlyUse7,
|
||||
requireSpecOf,
|
||||
resolveImport,
|
||||
splitRequireCallOf,
|
||||
} from "../program.js";
|
||||
import { createRequireCalleeFileOf } from "./builtins/module-bindings.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
@@ -43,6 +45,332 @@ export interface InlineRequire {
|
||||
dep: ts.SourceFile;
|
||||
}
|
||||
|
||||
/** A top-level `var m; m = require("./x")` binding. Node evaluates the
|
||||
* require at the assignment; the binding is alias plumbing exactly like
|
||||
* `const m = require("./x")` for every later read. */
|
||||
export interface SplitRequireBinding {
|
||||
decl: ts.VariableDeclaration;
|
||||
assign: ts.BinaryExpression;
|
||||
call: ts.CallExpression;
|
||||
spec: string;
|
||||
dep: ts.SourceFile;
|
||||
}
|
||||
|
||||
function symbolOfIdentifierValue(lowerer: Lowerer, ident: ts.Identifier): ts.Symbol | undefined {
|
||||
const parent = ident.parent;
|
||||
// A destructuring shorthand target resolves to the property symbol at
|
||||
// the name, but reads/writes the variable through its value symbol.
|
||||
if (ts.isShorthandPropertyAssignment(parent) && parent.name === ident) {
|
||||
return (
|
||||
lowerer.checker.getShorthandAssignmentValueSymbol(parent) ??
|
||||
lowerer.checker.getSymbolAtLocation(ident)
|
||||
);
|
||||
}
|
||||
return lowerer.checker.getSymbolAtLocation(ident);
|
||||
}
|
||||
|
||||
function destructuringContainerOf(node: ts.Node): ts.Node | null {
|
||||
const parent = node.parent;
|
||||
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return parent;
|
||||
if (ts.isPropertyAssignment(parent) && parent.initializer === node) return parent;
|
||||
if (ts.isSpreadElement(parent) && parent.expression === node) return parent;
|
||||
if (ts.isSpreadAssignment(parent) && parent.expression === node) return parent;
|
||||
return null;
|
||||
}
|
||||
|
||||
function writeTargetOf(node: ts.Node): ts.Node | null {
|
||||
let target = node;
|
||||
for (;;) {
|
||||
const parent = target.parent;
|
||||
if (
|
||||
ts.isParenthesizedExpression(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isObjectLiteralExpression(parent)
|
||||
) {
|
||||
target = parent;
|
||||
continue;
|
||||
}
|
||||
const container = destructuringContainerOf(target);
|
||||
if (container === null) break;
|
||||
target = container;
|
||||
}
|
||||
const parent = target.parent;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === target &&
|
||||
ts.isAssignmentOperator(parent.operatorToken.kind)
|
||||
)
|
||||
return parent;
|
||||
if (
|
||||
(ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) &&
|
||||
parent.operand === target &&
|
||||
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
parent.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
return parent;
|
||||
if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target)
|
||||
return parent;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The split form of a top-level require binding: `var m; m = require("./x")`.
|
||||
* Exactly one write, at a top-level `m = require("<literal>")` statement.
|
||||
* Null for the ordinary `const m = require("./x")` declaration and for
|
||||
* every form that needs runtime module selection. */
|
||||
export function splitRequireBindingOf(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): SplitRequireBinding | null {
|
||||
// Cheap shape gate before the cache: ordinary declarations never enter the
|
||||
// per-Lowerer map.
|
||||
if (decl.initializer !== undefined || !ts.isIdentifier(decl.name)) return null;
|
||||
const list = decl.parent as ts.VariableDeclarationList;
|
||||
if ((list.flags & ts.NodeFlags.Const) !== 0) return null;
|
||||
const stmt = list.parent;
|
||||
if (!ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null;
|
||||
const cached = lowerer.cjsSplitRequireBindings.get(decl);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = computeSplitRequireBinding(lowerer, decl);
|
||||
lowerer.cjsSplitRequireBindings.set(decl, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function computeSplitRequireBinding(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): SplitRequireBinding | null {
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return null;
|
||||
if (!sf.text.includes("require")) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
|
||||
if (symbol === undefined) return null;
|
||||
// Merged declarations make the binding's initialization order ambiguous;
|
||||
// keep ordinary storage instead of guessing.
|
||||
let declarations = 0;
|
||||
for (const other of symbol.declarations) {
|
||||
if (!ts.isVariableDeclaration(other) || ++declarations > 1) return null;
|
||||
}
|
||||
if (declarations !== 1) return null;
|
||||
|
||||
// Find the one top-level `m = ...` statement first: most uninitialized
|
||||
// variables never reach that shape, so they avoid the full-file write scan.
|
||||
let assign: ts.BinaryExpression | null = null;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isExpressionStatement(stmt)) continue;
|
||||
const expr = stmt.expression;
|
||||
if (
|
||||
!ts.isBinaryExpression(expr) ||
|
||||
expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isIdentifier(expr.left) ||
|
||||
expr.left.text !== decl.name.text ||
|
||||
lowerer.checker.getSymbolAtLocation(expr.left) !== symbol
|
||||
)
|
||||
continue;
|
||||
if (assign !== null) return null;
|
||||
assign = expr;
|
||||
}
|
||||
if (assign === null) return null;
|
||||
|
||||
const req = inlineProgramRequireOf(lowerer, assign.right);
|
||||
if (req === null) return null;
|
||||
// Preflight treats the same bindings as require declarations (module edge,
|
||||
// cycle admission, early-read fence); a binding it does not know keeps
|
||||
// ordinary storage.
|
||||
if (splitRequireCallOf(lowerer.program, decl) !== req.call) return null;
|
||||
|
||||
// Any other write makes the value depend on runtime control flow.
|
||||
let writes = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isIdentifier(node) || node.text !== decl.name.text) return;
|
||||
if (symbolOfIdentifierValue(lowerer, node) !== symbol) return;
|
||||
if (writeTargetOf(node) === null) return;
|
||||
if (++writes > 1) return "stop";
|
||||
});
|
||||
if (writes !== 1) return null;
|
||||
|
||||
return { decl, assign, call: req.call, spec: req.spec, dep: req.dep };
|
||||
}
|
||||
|
||||
/** The split binding an identifier names, or null. */
|
||||
export function splitRequireBindingOfIdent(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): SplitRequireBinding | null {
|
||||
const symbol = symbolOfIdentifierValue(lowerer, ident);
|
||||
return symbol === undefined ? null : splitBindingOfSymbol(lowerer, symbol);
|
||||
}
|
||||
|
||||
/** The exact top-level assignment statement that initializes `expr`, or null. */
|
||||
export function splitRequireAssignmentOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): SplitRequireBinding | null {
|
||||
if (
|
||||
!ts.isBinaryExpression(expr) ||
|
||||
expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isIdentifier(expr.left)
|
||||
)
|
||||
return null;
|
||||
const bindingOfIdent = splitRequireBindingOfIdent(lowerer, expr.left);
|
||||
return bindingOfIdent !== null && bindingOfIdent.assign === expr ? bindingOfIdent : null;
|
||||
}
|
||||
|
||||
/** The whole-export symbol a module's `module.exports = <value>` names,
|
||||
* or null when the module is a table. */
|
||||
export function wholeExportSymbolOfModule(lowerer: Lowerer, dep: ts.SourceFile): ts.Symbol | null {
|
||||
const cached = lowerer.cjsWholeExportSymbols.get(dep);
|
||||
if (cached !== undefined) return cached;
|
||||
if (cjsNamespaceModule(lowerer, dep)) {
|
||||
lowerer.cjsWholeExportSymbols.set(dep, null);
|
||||
return null;
|
||||
}
|
||||
const moduleSymbol = lowerer.checker.getSymbolAtLocation(dep);
|
||||
let symbol = moduleSymbol?.getExports()?.get("export=" as ts.__String);
|
||||
if (symbol === undefined) {
|
||||
lowerer.cjsWholeExportSymbols.set(dep, null);
|
||||
return null;
|
||||
}
|
||||
if (symbol.flags & ts.SymbolFlags.Alias) symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
lowerer.cjsWholeExportSymbols.set(dep, symbol);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
function splitBindingOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): SplitRequireBinding | null {
|
||||
const decl = symbol.declarations.find(ts.isVariableDeclaration);
|
||||
return decl === undefined ? null : splitRequireBindingOf(lowerer, decl);
|
||||
}
|
||||
|
||||
/** The target whole-export symbol behind a split-binding symbol. */
|
||||
function splitWholeExportSymbolOfSymbol(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
|
||||
const binding = splitBindingOfSymbol(lowerer, symbol);
|
||||
return binding === null ? null : wholeExportSymbolOfModule(lowerer, binding.dep);
|
||||
}
|
||||
|
||||
/** True when a CommonJS module has a live member export (`exports.x` or
|
||||
* `module.exports.x`) after all `module.exports =` replacements. Those
|
||||
* members need a mutable whole-export root. */
|
||||
export function cjsHasLiveMemberExports(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
const cached = lowerer.cjsLiveMemberExports.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const stmts = sf.statements.nodes;
|
||||
let lastTable = -1;
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i;
|
||||
}
|
||||
let live = false;
|
||||
// Only members attached after the last replacement survive in Node.
|
||||
if (lastTable >= 0) {
|
||||
for (let i = lastTable + 1; i < stmts.length; i++) {
|
||||
const stmt = stmts[i]!;
|
||||
if (cjsExportAssignmentOf(stmt)?.kind === "member" && cjsExportDiscardReason(stmt) === null) {
|
||||
live = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lowerer.cjsLiveMemberExports.set(sf, live);
|
||||
return live;
|
||||
}
|
||||
|
||||
/** The wrapper's export= symbol when a split binding is re-exported whole
|
||||
* (`module.exports = m`) and that assignment owns storage for later member
|
||||
* writes. Importers must read the wrapper's storage, not the split
|
||||
* binding's underlying value. */
|
||||
function splitWholeExportStorageSymbolOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
|
||||
const binding = splitBindingOfSymbol(lowerer, symbol);
|
||||
if (binding === null) return null;
|
||||
if (wholeExportSymbolOfModule(lowerer, binding.dep) === null) return null;
|
||||
const sf = ts.getSourceFileOfNode(binding.decl);
|
||||
if (!cjsHasLiveMemberExports(lowerer, sf)) return null;
|
||||
const whole = cjsWholeExportValueOf(sf);
|
||||
if (whole === null) return null;
|
||||
let rhs: ts.Expression = whole.rhs;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
if (!ts.isIdentifier(rhs) || lowerer.checker.getSymbolAtLocation(rhs) !== symbol) return null;
|
||||
const moduleSymbol = lowerer.checker.getSymbolAtLocation(sf);
|
||||
let exportSymbol = moduleSymbol?.getExports()?.get("export=" as ts.__String);
|
||||
if (exportSymbol === undefined) return null;
|
||||
if (exportSymbol.flags & ts.SymbolFlags.Alias)
|
||||
exportSymbol = lowerer.checker.getAliasedSymbol(exportSymbol);
|
||||
return exportSymbol;
|
||||
}
|
||||
|
||||
/** The source file whose `module.exports =` assignment declares `symbol`. */
|
||||
function moduleExportSourceFileOf(symbol: ts.Symbol): ts.SourceFile | null {
|
||||
for (const decl of symbol.declarations) {
|
||||
if (
|
||||
ts.isBinaryExpression(decl) &&
|
||||
isModuleExportsAccess(decl.left) &&
|
||||
ts.isExpressionStatement(decl.parent) &&
|
||||
ts.isSourceFile(decl.parent.parent)
|
||||
)
|
||||
return decl.getSourceFile();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** One hop from a wrapper's export= symbol to the value it re-exports
|
||||
* (`module.exports = require(...)` or `= <split binding>`), or null. A
|
||||
* wrapper with live members owns storage for them, so it is not followed. */
|
||||
function wholeReexportTargetOf(lowerer: Lowerer, symbol: ts.Symbol): ts.Symbol | null {
|
||||
const sf = moduleExportSourceFileOf(symbol);
|
||||
if (sf === null) return null;
|
||||
// A wrapper with live members owns storage for them; importers must keep
|
||||
// reading that root rather than the inner value.
|
||||
if (cjsHasLiveMemberExports(lowerer, sf)) return null;
|
||||
const whole = cjsWholeExportValueOf(sf);
|
||||
if (whole === null) return null;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
if (inner !== null) return wholeExportSymbolOfModule(lowerer, inner.dep);
|
||||
let rhs: ts.Expression = whole.rhs;
|
||||
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
||||
if (!ts.isIdentifier(rhs)) return null;
|
||||
const rhsSymbol = lowerer.checker.getSymbolAtLocation(rhs);
|
||||
return rhsSymbol === undefined ? null : splitWholeExportSymbolOfSymbol(lowerer, rhsSymbol);
|
||||
}
|
||||
|
||||
/** Follow split bindings and whole re-exports to the ultimate value symbol. */
|
||||
function followSplitWholeTarget(lowerer: Lowerer, candidate: ts.Symbol | null): ts.Symbol | null {
|
||||
if (candidate === null) return null;
|
||||
let symbol = candidate;
|
||||
const seen = new Set<ts.Symbol>();
|
||||
for (let depth = 0; depth < 32 && !seen.has(symbol); depth++) {
|
||||
seen.add(symbol);
|
||||
const next =
|
||||
splitWholeExportSymbolOfSymbol(lowerer, symbol) ?? wholeReexportTargetOf(lowerer, symbol);
|
||||
if (next === null) break;
|
||||
symbol = next;
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
|
||||
/** The target whole-export symbol behind `module.exports = <split binding>`,
|
||||
* letting importers of that wrapper resolve to the real value symbol. */
|
||||
export function splitWholeExportAliasTargetOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol,
|
||||
): ts.Symbol | null {
|
||||
const storage = splitWholeExportStorageSymbolOf(lowerer, symbol);
|
||||
if (storage !== null) return followSplitWholeTarget(lowerer, storage);
|
||||
const direct = followSplitWholeTarget(lowerer, splitWholeExportSymbolOfSymbol(lowerer, symbol));
|
||||
if (direct !== null) return direct;
|
||||
return followSplitWholeTarget(lowerer, wholeReexportTargetOf(lowerer, symbol));
|
||||
}
|
||||
|
||||
/** The first read of a split binding that code before its assignment can
|
||||
* reach, using the same reachability model as ordinary require bindings. */
|
||||
export function splitRequireEarlyReadOf(
|
||||
lowerer: Lowerer,
|
||||
binding: SplitRequireBinding,
|
||||
): ts.Identifier | null {
|
||||
const sf = ts.getSourceFileOfNode(binding.decl);
|
||||
const stmt = binding.assign.parent;
|
||||
// `binding` was recognized only from a top-level `m = require(...)`
|
||||
// statement, so the statement index is always present.
|
||||
const index = sf.statements.nodes.indexOf(stmt as ts.ExpressionStatement);
|
||||
return requireBindingEarlyUse7(lowerer.program, sf, index, binding.decl, []);
|
||||
}
|
||||
|
||||
/** A `require("<literal>")` call of the CommonJS global in a CommonJS JS
|
||||
* file whose target is a compiled program module (relative, aliased, or an
|
||||
* opted-in --npm-static package). Builtins, JSON documents and
|
||||
@@ -108,13 +436,6 @@ export function inlineNamespaceRequireOf(
|
||||
/** `value` evaluated after the required module's run-once %init (and its
|
||||
* module-registry link) at the require's own position. */
|
||||
export function withRequireInit(lowerer: Lowerer, req: InlineRequire, value: IrExpr): IrExpr {
|
||||
if (lowerer.inlineRequireCycleCalls.has(req.call)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
req.call,
|
||||
`require() of '${req.spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
|
||||
);
|
||||
}
|
||||
// Every inline program require is a preflight module edge, so its module
|
||||
// has an initializer; never read its exports without evaluating it.
|
||||
if (!lowerer.initNameOf.has(req.dep)) {
|
||||
|
||||
@@ -25,6 +25,11 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
|
||||
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
|
||||
import {
|
||||
splitRequireBindingOfIdent,
|
||||
wholeExportSymbolOfModule,
|
||||
wholeExportValue,
|
||||
} from "../cjs-inline-require.js";
|
||||
import { captureContextArguments } from "../function-context.js";
|
||||
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
|
||||
import {
|
||||
@@ -979,6 +984,27 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// Client`): the member holds that binding's value.
|
||||
const memberAlias = cjsMemberAliasValueOf(lowerer, expr);
|
||||
if (memberAlias !== null) return lowerer.lowerExpr(memberAlias);
|
||||
// `var m; m = require("./x")` as a bare value: a whole-export target is
|
||||
// the exported value; a table target has no first-class module object.
|
||||
{
|
||||
const split = splitRequireBindingOfIdent(lowerer, expr);
|
||||
if (split !== null) {
|
||||
if (wholeExportSymbolOfModule(lowerer, split.dep) !== null) {
|
||||
const value = wholeExportValue(lowerer, split.dep, expr);
|
||||
if (value !== null) return value;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`the whole export of '${split.spec}' as a value (its 'module.exports =' value has no static representation)`,
|
||||
);
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Preflight guarantees no unresolved identifiers; anything else here
|
||||
// is a blocked declaration's binding (the SC2004 cascade) or a
|
||||
// binding form we don't model yet.
|
||||
|
||||
@@ -43,10 +43,14 @@ import {
|
||||
} from "./cycle-initialization.js";
|
||||
import { markRequireEarlyBindings } from "./require-initialization.js";
|
||||
import {
|
||||
cjsHasLiveMemberExports,
|
||||
cjsTableMethodEntry,
|
||||
inlineProgramRequireOf,
|
||||
nestedCjsExportAssignmentsOf,
|
||||
requireNestedPatternsOf,
|
||||
splitRequireBindingOf,
|
||||
splitRequireBindingOfIdent,
|
||||
wholeExportSymbolOfModule,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js";
|
||||
@@ -1597,14 +1601,26 @@ export function collectGlobals(
|
||||
// require() of a program module is not: the exporter re-exports
|
||||
// that module's exports, which requirers resolve through the
|
||||
// checker's alias (the statement lowers to the dep's %init).
|
||||
// A split binding re-exported whole with later member writes needs
|
||||
// a mutable root: `module.exports = m; module.exports.extra = 1`
|
||||
// extends the SAME object Node exposes. Without later members the
|
||||
// binder is pure alias plumbing (no storage).
|
||||
const splitWholeBinding = ts.isIdentifier(rhs)
|
||||
? splitRequireBindingOfIdent(lowerer, rhs)
|
||||
: null;
|
||||
const hasMemberExports = cjsHasLiveMemberExports(lowerer, sf);
|
||||
const splitWhole =
|
||||
splitWholeBinding !== null &&
|
||||
wholeExportSymbolOfModule(lowerer, splitWholeBinding.dep) !== null &&
|
||||
hasMemberExports;
|
||||
const factoryValued =
|
||||
!ts.isIdentifier(rhs) &&
|
||||
(!ts.isIdentifier(rhs) || splitWhole) &&
|
||||
!ts.isClassExpression(rhs) &&
|
||||
!ts.isObjectLiteralExpression(rhs) &&
|
||||
!(
|
||||
requireSpecOf(rhs) !== null &&
|
||||
inlineProgramRequireOf(lowerer, rhs) !== null &&
|
||||
!sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "member")
|
||||
!hasMemberExports
|
||||
) &&
|
||||
cjsExportTargetLiteral(rhs, sf) === null;
|
||||
if (cjsScalarLiteral(rhs) || fnValued || factoryValued) {
|
||||
@@ -1617,6 +1633,9 @@ export function collectGlobals(
|
||||
// A returned object remains the live CommonJS root; later
|
||||
// exports.member writes must update that same open object.
|
||||
if (factoryValued && t?.kind === "record" && lowerer.dynConvertible(t)) t = DYN;
|
||||
// A split whole-export root is extended by later member writes;
|
||||
// keep it checked-dynamic so those writes reach the same object.
|
||||
if (splitWhole && t && t.kind !== "dyn") t = DYN;
|
||||
if (t && t.kind !== "void") {
|
||||
const g: IrGlobal = {
|
||||
id: `%g.${tag}exports`,
|
||||
@@ -2708,6 +2727,7 @@ function noteVarGlobalEntryInit(lowerer: Lowerer, sf: ts.SourceFile, g: IrGlobal
|
||||
* plumbing (lowerVarDeclarator): no storage, the reads resolve through to
|
||||
* the module. JSON documents and native FFI requires keep their storage. */
|
||||
function requireAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (splitRequireBindingOf(lowerer, decl) !== null) return true;
|
||||
return (
|
||||
decl.initializer !== undefined &&
|
||||
requireSpecOf(decl.initializer) !== null &&
|
||||
|
||||
@@ -39,6 +39,11 @@ import {
|
||||
cjsTableMethodEntry,
|
||||
inlineProgramRequireOf,
|
||||
requireNestedPatternsOf,
|
||||
splitRequireAssignmentOf,
|
||||
splitRequireBindingOf,
|
||||
splitRequireBindingOfIdent,
|
||||
splitRequireEarlyReadOf,
|
||||
wholeExportSymbolOfModule,
|
||||
} from "./cjs-inline-require.js";
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
@@ -1934,6 +1939,23 @@ function lowerVarDeclarator(
|
||||
isConst: boolean,
|
||||
isLet: boolean,
|
||||
): IrStmt[] {
|
||||
// `var m; m = require("./x")`: the declaration is alias plumbing; the
|
||||
// assignment owns the module's run-once %init (cjs-inline-require.ts).
|
||||
// A read that code before the assignment can reach keeps Node's hoisted
|
||||
// `undefined` divergence visible at build time, like a var require
|
||||
// declaration below runnable code.
|
||||
const split = splitRequireBindingOf(lowerer, decl);
|
||||
if (split !== null) {
|
||||
const early = splitRequireEarlyReadOf(lowerer, split);
|
||||
if (early !== null) {
|
||||
lowerer.unsupported(
|
||||
"SC1013",
|
||||
early,
|
||||
`var require() binding '${(decl.name as ts.Identifier).text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — assign the require above the code that can run first)`,
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
// The canonical ESM bridge to a compiled program module. Like a
|
||||
// top-level CommonJS require declaration, the binding is alias
|
||||
// plumbing while the dependency evaluates at this statement's
|
||||
@@ -7537,6 +7559,26 @@ function lowerCjsSingleValueExport(
|
||||
return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc };
|
||||
}
|
||||
if (ts.isIdentifier(rhs)) {
|
||||
const split = splitRequireBindingOfIdent(lowerer, rhs);
|
||||
if (split !== null) {
|
||||
if (wholeExportSymbolOfModule(lowerer, split.dep) === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
rhs,
|
||||
`the exports object of '${split.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(split.spec)}).member directly)`,
|
||||
);
|
||||
}
|
||||
const root = lowerer.globalsByDeclNode.get(expr);
|
||||
if (root !== undefined) {
|
||||
return {
|
||||
kind: "assign",
|
||||
localId: root.id,
|
||||
value: lowerer.lowerExprExpecting(rhs, root.type),
|
||||
loc: locOf(rhs),
|
||||
};
|
||||
}
|
||||
return { kind: "block", body: [], loc };
|
||||
}
|
||||
const sym = lowerer.resolveValueSymbol(rhs);
|
||||
if (!sym) return null;
|
||||
if (
|
||||
@@ -8447,6 +8489,16 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
if (!ts.isIdentifier(expr.left)) {
|
||||
lowerer.unsupported("SC1090", expr.left, "assignment to non-variables");
|
||||
}
|
||||
const splitAssign = splitRequireAssignmentOf(lowerer, expr);
|
||||
if (splitAssign !== null) {
|
||||
return (
|
||||
lowerer.requireInitStmt(splitAssign.spec, splitAssign.call) ?? {
|
||||
kind: "block",
|
||||
body: [],
|
||||
loc: locOf(expr),
|
||||
}
|
||||
);
|
||||
}
|
||||
const target = lowerer.resolveWritable(expr.left);
|
||||
if (!target)
|
||||
lowerer.rejectUnresolved(
|
||||
|
||||
@@ -344,10 +344,13 @@ import {
|
||||
} from "./lower-modules.js";
|
||||
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
|
||||
import {
|
||||
type SplitRequireBinding,
|
||||
cjsNamespaceModule,
|
||||
cjsWholeExportValueOf,
|
||||
inlineNamespaceRequireOf,
|
||||
inlineProgramRequireOf,
|
||||
splitRequireBindingOf,
|
||||
splitWholeExportAliasTargetOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
import { tscInteropModuleOf } from "./cjs-tsc-interop.js";
|
||||
import { usesCjsExportsIdentity } from "./cjs-exports-object.js";
|
||||
@@ -2292,6 +2295,14 @@ export class Lowerer {
|
||||
/** Per CommonJS file using top-level `this`: the global holding the
|
||||
* object that stands for its original exports (lowerCjsTopLevelThis). */
|
||||
readonly cjsExportsThis = new Map<ts.SourceFile, IrGlobal>();
|
||||
/** `var m; m = require("./x")` alias bindings (cjs-inline-require.ts).
|
||||
* Null entries cache declarations that keep ordinary storage. */
|
||||
readonly cjsSplitRequireBindings = new Map<ts.VariableDeclaration, SplitRequireBinding | null>();
|
||||
/** Per-module whole-export symbol (cjs-inline-require.ts). Null means the
|
||||
* module's exports are a table. */
|
||||
readonly cjsWholeExportSymbols = new Map<ts.SourceFile, ts.Symbol | null>();
|
||||
/** Per-module live member-export classification (cjs-inline-require.ts). */
|
||||
readonly cjsLiveMemberExports = new Map<ts.SourceFile, boolean>();
|
||||
/** Inline program-module require calls whose %init the enclosing member
|
||||
* read or member call is already sequencing (cjs-inline-require.ts). */
|
||||
readonly inlineRequireInitDone = new Set<ts.CallExpression>();
|
||||
@@ -4136,6 +4147,11 @@ export class Lowerer {
|
||||
// registry (globals, fn signatures, classes) applies unchanged.
|
||||
const cjsValue = this.cjsExportValueSymbol(symbol);
|
||||
if (cjsValue) symbol = cjsValue;
|
||||
// `var m; m = require("./x"); module.exports = m` over a whole-export
|
||||
// target: TS types the wrapper's export= as `any`, so reconnect it to
|
||||
// the target's value symbol explicitly.
|
||||
const splitAlias = splitWholeExportAliasTargetOf(this, symbol);
|
||||
if (splitAlias) symbol = splitAlias;
|
||||
this.flushDeferred(symbol);
|
||||
return symbol;
|
||||
}
|
||||
@@ -4529,8 +4545,8 @@ export class Lowerer {
|
||||
const decl = decls.find(ts.isImportClause) ?? decls[0];
|
||||
if (!decl) return null;
|
||||
if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl);
|
||||
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer)
|
||||
return null;
|
||||
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return null;
|
||||
if (decl.initializer === undefined) return splitRequireBindingOf(this, decl)?.dep ?? null;
|
||||
{
|
||||
const interop = tscInteropModuleOf(this, expr);
|
||||
if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null;
|
||||
@@ -4635,6 +4651,9 @@ export class Lowerer {
|
||||
// a binding (cjs-inline-require.ts sequences the module's %init).
|
||||
if (!ts.isIdentifier(expr)) return this.cjsModuleRefDepOf(expr) !== null;
|
||||
const sym = this.checker.getSymbolAtLocation(expr);
|
||||
const splitDecl = sym?.declarations.find(ts.isVariableDeclaration);
|
||||
const split = splitDecl === undefined ? null : splitRequireBindingOf(this, splitDecl);
|
||||
if (split !== null) return cjsNamespaceModule(this, split.dep);
|
||||
if (this.localModuleBindingDepOf(expr) === null) return false;
|
||||
if (this.createRequireWholeExportSymbolOf(expr) !== null) return false;
|
||||
// SINGLE-VALUE exporters (`module.exports = Countdown` / `= double` /
|
||||
@@ -4942,6 +4961,13 @@ export class Lowerer {
|
||||
* their external dependencies by name). Null for everything else
|
||||
* (builtins load nothing; the rest kept its preflight fence). */
|
||||
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
|
||||
if (this.inlineRequireCycleCalls.has(node)) {
|
||||
this.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`require() of '${spec}' while it is still evaluating (a require cycle: Node answers its partial exports, which the static export table does not model)`,
|
||||
);
|
||||
}
|
||||
if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node);
|
||||
// Project requires resolve within the program; another bare require can
|
||||
// be a program-module edge when it names an opted-in --npm-static package
|
||||
|
||||
@@ -228,7 +228,8 @@ function detectAutoPackages(
|
||||
} else if (mode === "lib" && !viaTransitive) {
|
||||
// CJS packages spell their dep edges as top-level requires; the
|
||||
// import-statement scan alone would miss every one of them.
|
||||
for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) });
|
||||
for (const req of requiresOf(stmt, load.program))
|
||||
edges.push({ spec: req.spec, loc: locOf(req.node) });
|
||||
}
|
||||
}
|
||||
// Transitive selection counts every literal require() and import()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
CallExpression,
|
||||
ExportDeclaration,
|
||||
Identifier,
|
||||
SourceFile,
|
||||
@@ -30,6 +31,13 @@ export class ProgramAnalysis {
|
||||
readonly requireEarlyReferences = new Map<VariableDeclaration, Identifier | null>();
|
||||
/** Per file: the first top-level statement that can run user code. */
|
||||
readonly requireFirstRunnable = new Map<SourceFile, number>();
|
||||
/** Per file: split require bindings (`var m; m = require("./x")`), from
|
||||
* declaration to the require call of their one assignment
|
||||
* (splitRequireBindings7). */
|
||||
readonly splitRequireBindings = new Map<
|
||||
SourceFile,
|
||||
ReadonlyMap<VariableDeclaration, CallExpression>
|
||||
>();
|
||||
|
||||
clear(): void {
|
||||
this.createRequireReasons.clear();
|
||||
@@ -41,5 +49,6 @@ export class ProgramAnalysis {
|
||||
this.moduleEarlyBindings = null;
|
||||
this.requireEarlyReferences.clear();
|
||||
this.requireFirstRunnable.clear();
|
||||
this.splitRequireBindings.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,17 +1423,31 @@ function isRequireStatement7(stmt: ts.Statement): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** A require() occurrence of a top-level statement. `split` marks the
|
||||
* assignment of a split require binding (splitRequireBindings7): `node`
|
||||
* is its require call and `decl` the binding's declaration. */
|
||||
interface RequireOccurrence7 {
|
||||
spec: string;
|
||||
node: ts.Node;
|
||||
decl: ts.VariableDeclaration | null;
|
||||
split?: true;
|
||||
}
|
||||
|
||||
/** The require() occurrences of one top-level statement: the declaration
|
||||
* forms and the bare side-effect call. Empty for everything else. */
|
||||
function requiresOf7(
|
||||
stmt: ts.Statement,
|
||||
): { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] {
|
||||
* forms, the bare side-effect call, and the assignment of a split require
|
||||
* binding. Empty for everything else. */
|
||||
function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] {
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
const split = splitRequireOfStatement7(program, stmt);
|
||||
if (split !== null)
|
||||
return [
|
||||
{ spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true },
|
||||
];
|
||||
const spec = requireSpecOf7(stmt.expression);
|
||||
return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : [];
|
||||
}
|
||||
if (!ts.isVariableStatement(stmt)) return [];
|
||||
const out: { spec: string; node: ts.Node; decl: ts.VariableDeclaration | null }[] = [];
|
||||
const out: RequireOccurrence7[] = [];
|
||||
for (const decl of stmt.declarationList.declarations.nodes) {
|
||||
const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null;
|
||||
if (spec !== null) out.push({ spec, node: decl, decl });
|
||||
@@ -1441,6 +1455,172 @@ function requiresOf7(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** CommonJS require bindings split into a declaration and one later
|
||||
* assignment (`var m; m = require("./x")`): a top-level `var` or `let`
|
||||
* declaration without initializer in a CommonJS file, assigned by exactly one
|
||||
* top-level `m = require("<literal>")` statement of the require global over
|
||||
* a compiled program module, and written nowhere else. The binding holds the
|
||||
* module's exports from the assignment on, as `const m = require("./x")`
|
||||
* does, so preflight treats the assignment as that require: its module edge,
|
||||
* its cycle admission and its early-read fence. The lowering's recognizer
|
||||
* (splitRequireBindingOf in cjs-inline-require.ts) accepts only these
|
||||
* bindings. Keyed by declaration, with the assignment's require call. */
|
||||
function splitRequireBindings7(
|
||||
program: ts.Program,
|
||||
sf: ts.SourceFile,
|
||||
): ReadonlyMap<ts.VariableDeclaration, ts.CallExpression> {
|
||||
const memo = program.analysis.splitRequireBindings;
|
||||
const cached = memo.get(sf);
|
||||
if (cached !== undefined) return cached;
|
||||
const out = new Map<ts.VariableDeclaration, ts.CallExpression>();
|
||||
memo.set(sf, out);
|
||||
if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require"))
|
||||
return out;
|
||||
const checker = program.getTypeChecker();
|
||||
const stmts = sf.statements.nodes;
|
||||
const candidates = new Map<
|
||||
ts.Symbol,
|
||||
{ decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number }
|
||||
>();
|
||||
for (const stmt of stmts) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue;
|
||||
for (const decl of stmt.declarationList.declarations.nodes) {
|
||||
if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue;
|
||||
const symbol = checker.getSymbolAtLocation(decl.name);
|
||||
if (symbol === undefined || symbol.declarations.length !== 1) continue;
|
||||
candidates.set(symbol, { decl, call: null, assigns: 0 });
|
||||
}
|
||||
}
|
||||
if (candidates.size === 0) return out;
|
||||
// The one top-level `m = ...` statement, whose right side is the require.
|
||||
for (const stmt of stmts) {
|
||||
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
|
||||
const assign = stmt.expression;
|
||||
if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left))
|
||||
continue;
|
||||
const symbol = checker.getSymbolAtLocation(assign.left);
|
||||
const candidate = symbol !== undefined ? candidates.get(symbol) : undefined;
|
||||
if (candidate === undefined) continue;
|
||||
candidate.assigns++;
|
||||
let right = assign.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right))
|
||||
candidate.call = right;
|
||||
}
|
||||
const names = new Set<string>();
|
||||
for (const candidate of candidates.values()) {
|
||||
if (candidate.assigns === 1 && candidate.call !== null)
|
||||
names.add((candidate.decl.name as ts.Identifier).text);
|
||||
}
|
||||
if (names.size === 0) return out;
|
||||
// Exactly one write in the whole file: the assignment itself.
|
||||
const writes = new Map<ts.Symbol, number>();
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
|
||||
const parent = node.parent;
|
||||
const symbol =
|
||||
parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node
|
||||
? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node))
|
||||
: checker.getSymbolAtLocation(node);
|
||||
if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node))
|
||||
return undefined;
|
||||
writes.set(symbol, (writes.get(symbol) ?? 0) + 1);
|
||||
return undefined;
|
||||
});
|
||||
for (const [symbol, candidate] of candidates) {
|
||||
if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1)
|
||||
out.set(candidate.decl, candidate.call);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when `call` is a require of the CommonJS global over a compiled
|
||||
* program module: not a builtin, a JSON document or a declaration file. */
|
||||
function splitRequireTarget7(
|
||||
program: ts.Program,
|
||||
sf: ts.SourceFile,
|
||||
call: ts.CallExpression,
|
||||
): boolean {
|
||||
const spec = requireSpecOf7(call);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== null) return false;
|
||||
if (requireCallBindingKind7(program, call) !== "ambient") return false;
|
||||
const dep =
|
||||
resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require");
|
||||
return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json");
|
||||
}
|
||||
|
||||
/** The split require binding a top-level statement assigns, or null. */
|
||||
function splitRequireOfStatement7(
|
||||
program: ts.Program,
|
||||
stmt: ts.Statement,
|
||||
): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null {
|
||||
if (
|
||||
!ts.isExpressionStatement(stmt) ||
|
||||
!ts.isBinaryExpression(stmt.expression) ||
|
||||
!ts.isSourceFile(stmt.parent)
|
||||
)
|
||||
return null;
|
||||
let right = stmt.expression.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
for (const [decl, call] of splitRequireBindings7(program, stmt.parent))
|
||||
if (call === right) return { decl, call };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The top-level statement of a split require binding's assignment, from
|
||||
* its require call (`m = require("./x")` or `m = (require("./x"))`). */
|
||||
function splitRequireStatementOf7(call: ts.CallExpression): ts.Node | undefined {
|
||||
let node: ts.Node = call;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
return node.parent?.parent;
|
||||
}
|
||||
|
||||
/** The require call of a split require binding's declaration, or null. */
|
||||
function splitRequireCallOf7(
|
||||
program: ts.Program,
|
||||
decl: ts.VariableDeclaration,
|
||||
): ts.CallExpression | null {
|
||||
return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null;
|
||||
}
|
||||
|
||||
/** True when `id` is written: an assignment target (compound and
|
||||
* destructuring included), an update operand, or a for-in/of target. */
|
||||
function isWriteReference7(id: ts.Identifier): boolean {
|
||||
let n: ts.Node = id;
|
||||
for (;;) {
|
||||
const p: ts.Node | undefined = n.parent;
|
||||
if (p === undefined) return false;
|
||||
if (ts.isBinaryExpression(p)) {
|
||||
const k = p.operatorToken.kind;
|
||||
return (
|
||||
p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment
|
||||
);
|
||||
}
|
||||
if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) {
|
||||
return (
|
||||
(p.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
p.operator === ts.SyntaxKind.MinusMinusToken) &&
|
||||
p.operand === n
|
||||
);
|
||||
}
|
||||
if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n;
|
||||
if (
|
||||
ts.isParenthesizedExpression(p) ||
|
||||
ts.isArrayLiteralExpression(p) ||
|
||||
ts.isSpreadElement(p) ||
|
||||
ts.isSpreadAssignment(p) ||
|
||||
ts.isShorthandPropertyAssignment(p) ||
|
||||
ts.isObjectLiteralExpression(p) ||
|
||||
(ts.isPropertyAssignment(p) && p.initializer === n)
|
||||
) {
|
||||
n = p;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** `true`, or `!0`, its minified spelling. */
|
||||
function isTrueLiteral(expr: ts.Expression): boolean {
|
||||
return (
|
||||
@@ -1580,7 +1760,7 @@ function purePrefixDecl7(decl: ts.VariableDeclaration): boolean {
|
||||
function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean {
|
||||
if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true;
|
||||
if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive
|
||||
if (isRequireStatement7(s)) return true;
|
||||
if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true;
|
||||
if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) {
|
||||
if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) {
|
||||
const call = s.expression as ts.CallExpression;
|
||||
@@ -1707,12 +1887,20 @@ function requireEarlyReference7(
|
||||
const cached = memo.get(decl);
|
||||
if (cached !== undefined) return cached;
|
||||
let found: ts.Identifier | null = null;
|
||||
const stmt = decl.parent?.parent;
|
||||
const sf = ts.getSourceFileOfNode(decl);
|
||||
if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) {
|
||||
// A split require binding is bound at its assignment statement.
|
||||
const splitCall = splitRequireCallOf7(program, decl);
|
||||
const stmt = splitCall !== null ? splitRequireStatementOf7(splitCall) : decl.parent?.parent;
|
||||
if (
|
||||
stmt !== undefined &&
|
||||
(ts.isVariableStatement(stmt) || splitCall !== null) &&
|
||||
ts.isSourceFile(stmt.parent)
|
||||
) {
|
||||
const stmts = sf.statements.nodes;
|
||||
const k = stmts.indexOf(stmt);
|
||||
const declarations = stmt.declarationList.declarations.nodes;
|
||||
const k = stmts.indexOf(stmt as ts.Statement);
|
||||
const declarations = ts.isVariableStatement(stmt)
|
||||
? stmt.declarationList.declarations.nodes
|
||||
: [];
|
||||
// Earlier declarators run too: `const x = read(), dep = require('./dep')`.
|
||||
const precedingDecls = declarations.slice(0, declarations.indexOf(decl));
|
||||
let firstRunnable = program.analysis.requireFirstRunnable.get(sf);
|
||||
@@ -1765,6 +1953,12 @@ export function requireBindingEarlyUse7(
|
||||
if (sym) bindings.add(sym);
|
||||
}
|
||||
if (bindings.size === 0) return null;
|
||||
// A split require binding's assignment writes the binding; it reads nothing.
|
||||
let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl);
|
||||
while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent))
|
||||
splitWrite = splitWrite.parent;
|
||||
splitWrite =
|
||||
splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null;
|
||||
const stmts = sf.statements.nodes;
|
||||
const cjs = isCjsJsFile7(sf, program);
|
||||
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
||||
@@ -1988,7 +2182,7 @@ export function requireBindingEarlyUse7(
|
||||
const sym = symbolOf(n);
|
||||
if (sym === undefined) return undefined;
|
||||
if (bindings.has(sym)) {
|
||||
if (exactEarlyRead?.(n) === true) return undefined;
|
||||
if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined;
|
||||
hit = n;
|
||||
return "stop";
|
||||
}
|
||||
@@ -3499,7 +3693,7 @@ function cjsCycleRefusal7(
|
||||
}
|
||||
// Bindings of other members must stay unreachable from top-level code.
|
||||
for (const st of stmts) {
|
||||
for (const req of requiresOf7(st)) {
|
||||
for (const req of requiresOf7(st, program)) {
|
||||
if (req.decl === null) continue;
|
||||
const dep = resolveImport7(program, m, req.spec, "require");
|
||||
if (dep === null || !members.has(dep)) continue;
|
||||
@@ -5775,7 +5969,7 @@ function preflight7(load: LoadResult): {
|
||||
// module aliases cannot represent; createRequire bindings have no
|
||||
// checked form either.
|
||||
const earlyRequireFence = (
|
||||
req: { decl: ts.VariableDeclaration | null },
|
||||
req: RequireOccurrence7,
|
||||
bindingKind: RequireCallBindingKind7,
|
||||
loc: { file: string; start: number; end: number },
|
||||
): boolean => {
|
||||
@@ -5783,6 +5977,7 @@ function preflight7(load: LoadResult): {
|
||||
if (
|
||||
early === null ||
|
||||
(bindingKind !== "createRequire" &&
|
||||
req.split === undefined &&
|
||||
((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0)
|
||||
) {
|
||||
return false;
|
||||
@@ -5793,14 +5988,16 @@ function preflight7(load: LoadResult): {
|
||||
loc,
|
||||
bindingKind === "createRequire"
|
||||
? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)`
|
||||
: `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`,
|
||||
: req.split !== undefined
|
||||
? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)`
|
||||
: `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
for (let k = 0; k < stmts.length; k++) {
|
||||
const stmt = stmts[k]!;
|
||||
for (const req of requiresOf7(stmt)) {
|
||||
for (const req of requiresOf7(stmt, program)) {
|
||||
const loc = {
|
||||
file: sf.fileName,
|
||||
start: ts.tokenStartOfNode(req.node),
|
||||
@@ -5827,8 +6024,8 @@ function preflight7(load: LoadResult): {
|
||||
const jsonDep = resolveImport7(program, sf, req.spec, "require");
|
||||
if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) {
|
||||
if (bindingKind === "createRequire") continue;
|
||||
if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) {
|
||||
const others = requiresOf7(stmt).some((r) => {
|
||||
if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) {
|
||||
const others = requiresOf7(stmt, program).some((r) => {
|
||||
const d = resolveImport7(program, sf, r.spec, "require");
|
||||
return d === null || !d.fileName.endsWith(".json");
|
||||
});
|
||||
@@ -6000,8 +6197,12 @@ function preflight7(load: LoadResult): {
|
||||
// counts, and targets outside the compiled graph add nothing here:
|
||||
// their use sites keep their existing lowering fences.
|
||||
if (jsRequireFile && !isNodeEsmFile7(sf, program)) {
|
||||
// A split require binding's assignment is a statement-level require
|
||||
// edge (requiresOf7 above), parenthesized or not.
|
||||
const splitCalls = new Set(splitRequireBindings7(program, sf).values());
|
||||
for (const call of inlineRequiresOf7(sf)) {
|
||||
if (requireCallBindingKind7(program, call) !== "ambient") continue;
|
||||
if (splitCalls.has(call)) continue;
|
||||
const spec = requireSpecOf7(call)!;
|
||||
if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue;
|
||||
const lazy = inDeferredPosition7(call);
|
||||
@@ -6750,6 +6951,7 @@ export {
|
||||
requireSpecOf7 as requireSpecOf,
|
||||
isRequireStatement7 as isRequireStatement,
|
||||
requiresOf7 as requiresOf,
|
||||
splitRequireCallOf7 as splitRequireCallOf,
|
||||
isNodeEsmFile7 as isNodeEsmFile,
|
||||
resolveImport7 as resolveImport,
|
||||
resolveNpmImport7 as resolveNpmImport,
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/lib.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/whole.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/reexport.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/reexport-extra.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/chain.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/chain-wrap.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-binding/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/split-cjs-require-cycle/odd.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-cycle/even.cjs",
|
||||
"<repo>/tests/corpus/split-cjs-require-cycle/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/split-cjs-require-cycle/b.js",
|
||||
"<repo>/tests/diagnostics/split-cjs-require-cycle/main.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/split-cjs-require-cycle/main.js → <repo>/tests/diagnostics/split-cjs-require-cycle/b.js → <repo>/tests/diagnostics/split-cjs-require-cycle/main.js; the cycle closes through a require() edge: <repo>/tests/diagnostics/split-cjs-require-cycle/main.js uses its exports at <repo>/tests/diagnostics/split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/split-cjs-require-cycle/main.js",
|
||||
"start": 0,
|
||||
"end": 0
|
||||
},
|
||||
"milestone": "later",
|
||||
"hint": "ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge"
|
||||
}
|
||||
]
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/split-cjs-require-early/main.js"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/split-cjs-require-early/main.js",
|
||||
"start": 396,
|
||||
"end": 415
|
||||
},
|
||||
"milestone": "later"
|
||||
},
|
||||
{
|
||||
"code": "SC1013",
|
||||
"message": "require() binding 'wrapped' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/tests/diagnostics/split-cjs-require-early/main.js",
|
||||
"start": 530,
|
||||
"end": 549
|
||||
},
|
||||
"milestone": "later"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
var chained;
|
||||
chained = require("./chain.cjs");
|
||||
module.exports = chained;
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("./whole.cjs");
|
||||
@@ -0,0 +1,5 @@
|
||||
console.log("lib: init");
|
||||
exports.n = 7;
|
||||
exports.f = function (x) {
|
||||
return x * 2;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
console.log("main: start");
|
||||
var lib, whole;
|
||||
lib = require("./lib.cjs");
|
||||
whole = require("./whole.cjs");
|
||||
console.log("main: after", lib.n, lib.f(3), whole(4));
|
||||
exports.fromLib = lib.f;
|
||||
exports.whole = whole;
|
||||
console.log("main: exported", exports.fromLib(5), whole(6));
|
||||
console.log("main: identity", exports.fromLib === lib.f, exports.whole === whole);
|
||||
const re = require("./reexport.cjs");
|
||||
console.log("main: reexport", re(7));
|
||||
let later;
|
||||
later = require("./lib.cjs");
|
||||
console.log("main: let", later.n);
|
||||
const rx = require("./reexport-extra.cjs");
|
||||
console.log("main: reexport extra", rx(8), rx.extra);
|
||||
const splitObject = { whole };
|
||||
console.log("main: shorthand value", splitObject.whole(10));
|
||||
var libAgain;
|
||||
libAgain = require("./lib.cjs");
|
||||
const { n: splitN, f: splitF } = libAgain;
|
||||
console.log("main: destructured split", splitN, splitF(4));
|
||||
var chained;
|
||||
chained = require("./chain-wrap.cjs");
|
||||
console.log("main: chained", chained(11));
|
||||
|
||||
// Any later write, including a destructuring target, keeps ordinary storage.
|
||||
var shorthand;
|
||||
shorthand = require("./whole.cjs");
|
||||
({ shorthand } = { shorthand: "shorthand" });
|
||||
console.log("main: shorthand write", shorthand);
|
||||
var renamed;
|
||||
renamed = require("./whole.cjs");
|
||||
({ value: renamed } = { value: "renamed" });
|
||||
console.log("main: renamed write", renamed);
|
||||
var nested;
|
||||
nested = require("./whole.cjs");
|
||||
({ a: { nested } } = { a: { nested: "nested" } });
|
||||
console.log("main: nested write", nested);
|
||||
var rest;
|
||||
rest = require("./whole.cjs");
|
||||
({ ...rest } = { a: 1, b: 2 });
|
||||
console.log("main: rest write", rest.a, rest.b);
|
||||
var array;
|
||||
array = require("./whole.cjs");
|
||||
[array] = ["array"];
|
||||
console.log("main: array write", array);
|
||||
var arrayRest;
|
||||
arrayRest = require("./whole.cjs");
|
||||
[...arrayRest] = [1, 2];
|
||||
console.log("main: array-rest write", arrayRest[0], arrayRest[1]);
|
||||
var each;
|
||||
each = require("./whole.cjs");
|
||||
for ({ each } of [{ each: "each" }]) {}
|
||||
console.log("main: for-of write", each);
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -0,0 +1,6 @@
|
||||
var whole;
|
||||
whole = require("./whole.cjs");
|
||||
console.log("extra: before", whole.extra, whole(9));
|
||||
module.exports = whole;
|
||||
module.exports.extra = 1;
|
||||
console.log("extra: after", whole.extra, module.exports.extra);
|
||||
@@ -0,0 +1,4 @@
|
||||
console.log("reexport: init");
|
||||
var whole;
|
||||
whole = require("./whole.cjs");
|
||||
module.exports = whole;
|
||||
@@ -0,0 +1,4 @@
|
||||
console.log("whole: init");
|
||||
module.exports = function whole(x) {
|
||||
return x + 1;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
var odd;
|
||||
exports.name = "even";
|
||||
exports.isEven = function (n) {
|
||||
return n === 0 ? true : odd.isOdd(n - 1);
|
||||
};
|
||||
exports.describe = function (n) {
|
||||
return n + " is " + (exports.isEven(n) ? "even" : "odd") + " (asked " + odd.name + ")";
|
||||
};
|
||||
odd = require("./odd.cjs");
|
||||
console.log("even: loaded");
|
||||
@@ -0,0 +1,12 @@
|
||||
// Split require bindings (`var m; m = require("./x.cjs")`) that close a
|
||||
// CommonJS cycle: each module publishes its exports before its require, and
|
||||
// reads the other module's binding only in functions that run after both
|
||||
// modules finish, the cycle shape the const form compiles too. One
|
||||
// assignment parenthesizes its require.
|
||||
console.log("main: start");
|
||||
var even, odd;
|
||||
even = require("./even.cjs");
|
||||
odd = require("./odd.cjs");
|
||||
console.log(even.isEven(10), odd.isOdd(7), even.isEven(3));
|
||||
console.log(even.describe(4), "|", even.describe(9));
|
||||
console.log("odd calls", odd.calls(), even.name, odd.name);
|
||||
@@ -0,0 +1,12 @@
|
||||
var even;
|
||||
let calls = 0;
|
||||
exports.name = "odd";
|
||||
exports.isOdd = function (n) {
|
||||
calls++;
|
||||
return n === 0 ? false : even.isEven(n - 1);
|
||||
};
|
||||
exports.calls = function () {
|
||||
return calls;
|
||||
};
|
||||
even = (require("./even.cjs"));
|
||||
console.log("odd: loaded");
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -0,0 +1,7 @@
|
||||
var a;
|
||||
a = require('./main.js');
|
||||
exports.fromB = 'b-value';
|
||||
exports.readA = function () {
|
||||
return a.early + '/' + a.late;
|
||||
};
|
||||
console.log('b sees a.early:', a.early, 'a.late:', a.late);
|
||||
@@ -0,0 +1,11 @@
|
||||
// A split require binding (`var b; b = require("./b.js")`) whose require
|
||||
// closes a CommonJS cycle that top-level code can observe: b.js reads a.js's
|
||||
// partial exports while a.js is still evaluating, which the compiled export
|
||||
// table does not model. Preflight refuses it with SC1016, as it refuses the
|
||||
// `const b = require("./b.js")` form; tests/corpus/split-cjs-require-cycle
|
||||
// compiles the cycle shape both forms admit.
|
||||
exports.early = 'a-early';
|
||||
var b;
|
||||
b = require('./b.js');
|
||||
exports.late = 'a-late';
|
||||
console.log('a sees', b.fromB, b.readA());
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/** @param {string} who */
|
||||
function greet(who) {
|
||||
return `hello ${who}`;
|
||||
}
|
||||
|
||||
module.exports = { greet };
|
||||
@@ -0,0 +1,21 @@
|
||||
// A split require binding (`let lib;` then `lib = require(...)`) that code
|
||||
// above the assignment can reach: Node reads undefined there, a value the
|
||||
// compiled module alias cannot hold, so the assignment keeps a named SC1013
|
||||
// fence. Assigned before any such code, the binding compiles
|
||||
// (tests/corpus/split-cjs-require-binding).
|
||||
'use strict';
|
||||
|
||||
let lib;
|
||||
console.log('boot', helper());
|
||||
lib = require('./lib.js');
|
||||
|
||||
// The same with the require parenthesized.
|
||||
let wrapped;
|
||||
console.log('peek', wrapped === undefined);
|
||||
wrapped = (require('./lib.js'));
|
||||
|
||||
function helper() {
|
||||
return lib === undefined ? 'not loaded' : lib.greet('late');
|
||||
}
|
||||
|
||||
console.log(helper());
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -0,0 +1,7 @@
|
||||
split-cjs-require-cycle/main.js:1:1 - error SC1016: circular imports (split-cjs-require-cycle/main.js → split-cjs-require-cycle/b.js → split-cjs-require-cycle/main.js; the cycle closes through a require() edge: split-cjs-require-cycle/main.js uses its exports at split-cjs-require-cycle/main.js:10 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet
|
||||
|
||||
1 | // A split require binding (`var b; b = require("./b.js")`) whose require
|
||||
| ^
|
||||
2 | // closes a CommonJS cycle that top-level code can observe: b.js reads a.js's
|
||||
|
||||
hint: ES-module cycles of let/const/function bindings compile as-is; rewrite the named construct (a named import instead of a namespace, a const instead of a default-export expression or var, a class declared before the code that can reach it) or break the named edge
|
||||
@@ -0,0 +1,13 @@
|
||||
split-cjs-require-early/main.js:10:7 - error SC1013: require() binding 'lib' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet
|
||||
|
||||
9 | console.log('boot', helper());
|
||||
10 | lib = require('./lib.js');
|
||||
| ^~~~~~~~~~~~~~~~~~~
|
||||
11 |
|
||||
|
||||
split-cjs-require-early/main.js:15:12 - error SC1013: require() binding 'wrapped' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first) is not supported yet
|
||||
|
||||
14 | console.log('peek', wrapped === undefined);
|
||||
15 | wrapped = (require('./lib.js'));
|
||||
| ^~~~~~~~~~~~~~~~~~~
|
||||
16 |
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 1500, "san": 12000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2700, "san": 3200 }
|
||||
@@ -325,6 +325,35 @@ console.log(cells.join(","));
|
||||
);
|
||||
});
|
||||
|
||||
test("a split require closing a CommonJS cycle keeps the const form's SC1016", async () => {
|
||||
// Node answers the closing require with the dependency's partial exports
|
||||
// ({ fromA: 1 } at this point), which the static export model cannot
|
||||
// represent. Preflight treats the split assignment like `const a =
|
||||
// require("./a.cjs")`, so the cycle keeps that form's SC1016 instead of
|
||||
// reading uninitialized export storage. A cycle whose reads are all deferred
|
||||
// compiles (tests/corpus/split-cjs-require-cycle).
|
||||
const r = compileAndRun(
|
||||
"split-require-cycle",
|
||||
`require("./a.cjs");
|
||||
`,
|
||||
"cjs",
|
||||
{
|
||||
"a.cjs": `exports.fromA = 1;
|
||||
const b = require("./b.cjs");
|
||||
exports.fromB = b.fromB;
|
||||
`,
|
||||
"b.cjs": `exports.fromB = 2;
|
||||
var a;
|
||||
a = require("./a.cjs");
|
||||
console.log(a.fromA, a.fromB);
|
||||
`,
|
||||
},
|
||||
);
|
||||
await expect(r).rejects.toThrow(
|
||||
/SC1016: circular imports \(.*a\.cjs → .*b\.cjs → .*a\.cjs; the cycle closes through a require\(\) edge/,
|
||||
);
|
||||
});
|
||||
|
||||
test("an EFFECTFUL surplus argument is a named deferred fence (JS lane)", async () => {
|
||||
// Effect-free surplus args drop at compile time, JS-exact (corpus
|
||||
// 1760 — the test-string-decoder writeSequences shape). An effectful
|
||||
|
||||
Reference in New Issue
Block a user