From a2707ac8d6d78dba7055f2dbe8ea5f1cae9a6d66 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:07:01 -0700 Subject: [PATCH 1/4] Name the inner location in package-anchored npm-static refusals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A package-anchored preflight diagnostic that makes the build drop an npm package was reported only at the consumer's import, as `SC2013 cannot compile npm package 'ws' statically: SC1012: …`, with the package file and line lost. The refusal reason now carries them (`SC1012 at ws/lib/websocket.js:30: …`), in the SC2013 diagnostic and in the coverage report's package status. This is how the SC1012 construct behind ws, readable-stream, through2, socket.io and fastify was identified (nested require patterns, and a rest element in fastify), and how openai's SC1013 is located (`openai/internal/auth/x509-transport-state.mjs:2`). Co-Authored-By: Claude Opus 5.5 --- packages/compiler/src/frontend/pipeline.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 2ec9553f67..78971d7dbe 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -30,6 +30,21 @@ import { CONTRACT_TYPES_MODULE } from "./contract-types.js"; import { THREADS_MODULE } from "./threads-import.js"; import { EXPERIMENTAL_MODULE } from "./experimental-import.js"; +/** Where a package-anchored preflight diagnostic sits, as + * `:` — the SC2013 refusal that drops the package + * reports only at the consumer's import, so its reason carries the inner + * location. */ +function packageDiagWhere(load: LoadResult, loc: SrcLoc): string { + const file = loc.file.split("\\").join("/"); + const at = file.lastIndexOf("/node_modules/"); + const shown = at >= 0 ? file.slice(at + "/node_modules/".length) : file; + const text = load.program.getSourceFile(loc.file)?.text; + if (text === undefined) return shown; + let line = 1; + for (let i = 0; i < loc.start && i < text.length; i++) if (text.charCodeAt(i) === 10) line++; + return `${shown}:${line}`; +} + export interface ProgramLoadOptions { npmStatic?: readonly string[]; externalTypes?: Readonly> | undefined; @@ -344,7 +359,8 @@ function loadFrontend( const reasons = new Map(npmStaticOffenders()); for (const d of preflight) { const pkg = npmStaticPackageOfPath(d.loc.file); - if (pkg !== null && !reasons.has(pkg)) reasons.set(pkg, `${d.code}: ${d.message}`); + if (pkg !== null && !reasons.has(pkg)) + reasons.set(pkg, `${d.code} at ${packageDiagWhere(load, d.loc)}: ${d.message}`); } if (![...reasons.keys()].some((p) => effective.has(p))) { const named = new Map(); From fbcdb1d641bb2d54f34fb81db2dffa4b133acd32 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:29:28 -0700 Subject: [PATCH 2/4] Compile inline require(), computed and chained CommonJS exports, and a module's own exports object The npm compatibility probes ranked these CommonJS forms among the top blockers. They now compile with Node's semantics inside the static CommonJS model, where exports stay module globals or alias plumbing to declarations. Every remaining form keeps a refusal that names the construct. - Inline require() of program modules (cjs-inline-require.ts). `require("./x").f`, `.f(...)` and `new (require("./x").C)()` over a table module run the module's run-once %init at the call's position, then resolve the member like a binding does. `require("./x")` as a value over a single-value module is that value (`require("debug")("ns")`, table entries, construction). Inline requires are preflight module edges. An edge in a function body is lazy: it orders the dependency after the requiring module and does not close initialization cycles. Other require() calls name their construct instead of reporting an unlowerable 'Require' value. A package that nothing installed resolves throws MODULE_NOT_FOUND. - module.exports = require("./y") or a require binding (re-export), = TypeError (a builtin global), and = any computed value (`a || b`, ternary, `new X()`). Chained `exports.a = exports.b = v` evaluates v once. Export members assigned in value position (`const src = exports.src = []`) or inside functions write the export global. A fresh `exports = module.exports = {}` reset at the top of a module is a no-op. Any other rebinding of `exports` is refused by name. - A module's own exports object (cjs-exports-object.ts). Identity comparisons, typeof and truthiness of `exports` / `module.exports` use the object that top-level `this` already represents, while the module keeps its original exports. This covers the `root === module.exports` statement that used to vanish from `.call(this)` wrappers. `exports.f(x)` calls the member's export global when no assigned value can read `this`. Other value uses are refused by name, including in CommonJS files where the message used to say "ES module". - tsc interop (cjs-tsc-interop.ts). `const x_1 = __importDefault(require(...))` and `__importStar(...)` are require bindings, stamped and unstamped. The helper declarations lower to nothing once every call is such a binding. - Export tables: method entries compile, and `...require("./x")` spread entries now evaluate x at their place. Before, the module was never initialized. - TypeScript reads of members a JavaScript module attached to an exported function go through the function's own property table. - Nested require destructuring of program modules, `const { codes: { E } } = require("./errors")`, destructures the named export at the require (SC1012). `const { A } = ns` over a module binding snapshots the export. The capture-buffer fence tests merged from opt/cjs-exports-value used `root === module.exports`, which now compiles. They use eval() to keep testing the fence. Co-Authored-By: Claude Opus 5.5 --- .../frontend/lowering/cjs-exports-object.ts | 431 ++++++++++++++++++ .../frontend/lowering/cjs-inline-require.ts | 371 +++++++++++++++ .../src/frontend/lowering/cjs-tsc-interop.ts | 223 +++++++++ .../src/frontend/lowering/lower-calls.ts | 65 +++ .../src/frontend/lowering/lower-classes.ts | 9 + .../src/frontend/lowering/lower-exprs.ts | 140 +++++- .../src/frontend/lowering/lower-modules.ts | 85 +++- .../frontend/lowering/lower-node-module.ts | 18 + .../src/frontend/lowering/lower-stmts.ts | 195 +++++++- .../compiler/src/frontend/lowering/lowerer.ts | 47 +- packages/compiler/src/frontend/program.ts | 135 +++++- .../6150-cjs-inline-require/fallback.js | 6 + tests/corpus/6150-cjs-inline-require/inner.js | 4 + tests/corpus/6150-cjs-inline-require/late.js | 2 + tests/corpus/6150-cjs-inline-require/lib.js | 6 + tests/corpus/6150-cjs-inline-require/main.js | 55 +++ .../6150-cjs-inline-require/make-logger.js | 6 + .../6150-cjs-inline-require/package.json | 1 + tests/corpus/6150-cjs-inline-require/pick.js | 7 + tests/corpus/6150-cjs-inline-require/point.js | 6 + .../reexport-binding.js | 3 + .../reexport-inline.js | 2 + .../6150-cjs-inline-require/registry.js | 13 + .../corpus/6150-cjs-inline-require/shapes.js | 9 + tests/corpus/6150-cjs-inline-require/table.js | 4 + .../6150-cjs-inline-require/type-error.js | 1 + .../corpus/6150-cjs-inline-require/verify.js | 4 + tests/corpus/6151-cjs-export-chains/chain.js | 10 + .../6151-cjs-export-chains/constants.js | 1 + tests/corpus/6151-cjs-export-chains/main.js | 9 + .../6151-cjs-export-chains/package.json | 1 + tests/corpus/6151-cjs-export-chains/re.js | 15 + tests/corpus/6151-cjs-export-chains/sem.js | 16 + tests/corpus/6152-cjs-tsc-interop/fn.js | 3 + tests/corpus/6152-cjs-tsc-interop/main.js | 17 + .../corpus/6152-cjs-tsc-interop/package.json | 1 + tests/corpus/6152-cjs-tsc-interop/plain.js | 5 + tests/corpus/6152-cjs-tsc-interop/stamped.js | 12 + tests/corpus/6153-cjs-exports-object/lib.js | 13 + tests/corpus/6153-cjs-exports-object/main.js | 19 + .../6153-cjs-exports-object/package.json | 1 + .../constants.js | 8 + .../errors.js | 12 + .../main.js | 14 + .../package.json | 1 + .../6155-cjs-export-table-methods/lib.js | 20 + .../6155-cjs-export-table-methods/main.js | 5 + .../6155-cjs-export-table-methods/more.js | 4 + .../package.json | 1 + .../consumer.cjs | 4 + .../6156-cjs-function-members-from-ts/main.ts | 9 + .../package.json | 1 + .../status.cjs | 10 + tests/harness/corpus-costs.json | 14 + tests/harness/errors.test.ts | 88 +++- tests/harness/npm.test.ts | 8 +- 56 files changed, 2129 insertions(+), 41 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/cjs-exports-object.ts create mode 100644 packages/compiler/src/frontend/lowering/cjs-inline-require.ts create mode 100644 packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts create mode 100644 tests/corpus/6150-cjs-inline-require/fallback.js create mode 100644 tests/corpus/6150-cjs-inline-require/inner.js create mode 100644 tests/corpus/6150-cjs-inline-require/late.js create mode 100644 tests/corpus/6150-cjs-inline-require/lib.js create mode 100644 tests/corpus/6150-cjs-inline-require/main.js create mode 100644 tests/corpus/6150-cjs-inline-require/make-logger.js create mode 100644 tests/corpus/6150-cjs-inline-require/package.json create mode 100644 tests/corpus/6150-cjs-inline-require/pick.js create mode 100644 tests/corpus/6150-cjs-inline-require/point.js create mode 100644 tests/corpus/6150-cjs-inline-require/reexport-binding.js create mode 100644 tests/corpus/6150-cjs-inline-require/reexport-inline.js create mode 100644 tests/corpus/6150-cjs-inline-require/registry.js create mode 100644 tests/corpus/6150-cjs-inline-require/shapes.js create mode 100644 tests/corpus/6150-cjs-inline-require/table.js create mode 100644 tests/corpus/6150-cjs-inline-require/type-error.js create mode 100644 tests/corpus/6150-cjs-inline-require/verify.js create mode 100644 tests/corpus/6151-cjs-export-chains/chain.js create mode 100644 tests/corpus/6151-cjs-export-chains/constants.js create mode 100644 tests/corpus/6151-cjs-export-chains/main.js create mode 100644 tests/corpus/6151-cjs-export-chains/package.json create mode 100644 tests/corpus/6151-cjs-export-chains/re.js create mode 100644 tests/corpus/6151-cjs-export-chains/sem.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/fn.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/main.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/package.json create mode 100644 tests/corpus/6152-cjs-tsc-interop/plain.js create mode 100644 tests/corpus/6152-cjs-tsc-interop/stamped.js create mode 100644 tests/corpus/6153-cjs-exports-object/lib.js create mode 100644 tests/corpus/6153-cjs-exports-object/main.js create mode 100644 tests/corpus/6153-cjs-exports-object/package.json create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/constants.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/errors.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/main.js create mode 100644 tests/corpus/6154-cjs-require-nested-destructure/package.json create mode 100644 tests/corpus/6155-cjs-export-table-methods/lib.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/main.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/more.js create mode 100644 tests/corpus/6155-cjs-export-table-methods/package.json create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/main.ts create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/package.json create mode 100644 tests/corpus/6156-cjs-function-members-from-ts/status.cjs diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts new file mode 100644 index 0000000000..c3edafddcc --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -0,0 +1,431 @@ +/* A CommonJS module's own exports object, seen from inside the module. + * + * The compiled module system has no reflective exports object: each export + * is a module global (collectGlobals) or alias plumbing to a declaration. + * Inside the module that is enough for + * + * - member reads and member calls (`exports.f`, `exports.f(x)`, + * `module.exports.f(x)`): they are the member's export global, the + * storage every `exports.f =` statement assigns; + * - identity comparisons of the object itself (`root === module.exports`, + * `exports === module.exports`): while the module never replaces + * `module.exports` and never rebinds `exports`, both name the module's + * original exports object, which top-level `this` already represents + * (lowerCjsTopLevelThis). `typeof` and truthiness keep their existing + * lowerings. + * + * Every other use of the object as a value (passing it to a function, + * storing it, reading a member through an alias) would need a real object + * whose properties are the exports; those uses keep a named refusal. The + * dynamic CommonJS module object is the two-tier design's S9 stage. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + cjsExportAssignmentOf, + cjsExportTargetLiteral, + isCjsJsFile, + isModuleExportsAccess, + isRequireStatement, + locOf, +} from "../program.js"; +import { isCjsModuleGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js"; + +/** True for the CommonJS wrapper's `exports` binding (not shadowed by any + * source declaration) in a CommonJS JS file. */ +export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean { + if (!ts.isIdentifier(node) || node.text !== "exports") return false; + if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false; + const symbol = lowerer.checker.getSymbolAtLocation(node); + const decls = symbol ? lowerer.checker.declarationsOf(symbol) : []; + return !decls.some( + (d) => + !d.getSourceFile().isDeclarationFile && + (ts.isVariableDeclaration(d) || + ts.isParameter(d) || + ts.isBindingElement(d) || + ts.isFunctionDeclaration(d) || + ts.isClassDeclaration(d) || + ts.isImportSpecifier(d) || + ts.isNamespaceImport(d) || + ts.isImportEqualsDeclaration(d)), + ); +} + +/** `module.exports` over the CommonJS `module` global in a CommonJS file. */ +export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boolean { + return ( + isModuleExportsAccess(node) && + isCjsModuleGlobal(lowerer, node.expression) && + isCjsJsFile(node.getSourceFile(), lowerer.program) + ); +} + +/** The export global behind a same-module member access `exports.` / + * `module.exports.`, or null (identifier-valued members are alias + * plumbing with no storage, and other receivers are not the exports + * object). */ +export function cjsInFileExportGlobalOf( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrGlobal | null { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return null; + const sf = access.getSourceFile(); + const recv = + isModuleExportsAccess(access.expression) || + (ts.isIdentifier(access.expression) && + access.expression.text === "exports" && + !lowerer.resolveLocal(access.expression) && + !lowerer.globalOf(access.expression)); + if (!recv || !isCjsJsFile(sf, lowerer.program)) return null; + const sym = + lowerer.checker.getSymbolAtLocation(access.name) ?? + lowerer.cjsModuleExportSymbol(sf, access.name.text); + const resolved = + sym !== undefined && sym.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(sym) + : sym; + return ( + (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? + (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined) ?? + null + ); +} + +/** Whether `node` is an operand of an equality operator: only its identity + * is observed. */ +function identityUse(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined || !ts.isBinaryExpression(parent)) return false; + const op = parent.operatorToken.kind; + return ( + (op === ts.SyntaxKind.EqualsEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.EqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken) && + (parent.left === child || parent.right === child) + ); +} + +/** Uses whose answer does not need the object: `typeof`, `void`, and + * truthiness tests (their existing lowerings answer them). */ +function answeredWithoutObject(node: ts.Expression): boolean { + let child: ts.Node = node; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) { + child = parent; + parent = parent.parent; + } + if (parent === undefined) return false; + if (ts.isTypeOfExpression(parent) || ts.isVoidExpression(parent)) return true; + return truthinessOnly(child as ts.Expression); +} + +function truthinessOnly(node: ts.Expression): boolean { + let child: ts.Node = node; + for (let parent = node.parent; parent !== undefined; child = parent, parent = parent.parent) { + if (ts.isParenthesizedExpression(parent)) continue; + if (ts.isPrefixUnaryExpression(parent)) + return parent.operator === ts.SyntaxKind.ExclamationToken; + if ( + ts.isBinaryExpression(parent) && + (parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken || + parent.operatorToken.kind === ts.SyntaxKind.BarBarToken) + ) + continue; + if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) + return parent.expression === child; + if (ts.isForStatement(parent)) return parent.condition === child; + if (ts.isConditionalExpression(parent)) return parent.condition === child; + return false; + } + return false; +} + +/** Which of the module's exports bindings the file ever assigns (any + * nesting, compound operators included). */ +const assignmentsCache = new WeakMap(); + +function exportsAssignments( + lowerer: Lowerer, + sf: ts.SourceFile, +): { exports: boolean; moduleExports: boolean } { + const cached = assignmentsCache.get(sf); + if (cached !== undefined) return cached; + const out = { exports: false, moduleExports: false }; + assignmentsCache.set(sf, out); + ts.walkPreorder(sf, (node) => { + // A fresh `exports = module.exports = {}` reset leaves both bindings + // naming one (new, empty) exports object for the rest of the module. + if (ts.isExpressionStatement(node) && isFreshExportsReset(lowerer, node)) return "skip"; + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind < ts.SyntaxKind.FirstAssignment || + node.operatorToken.kind > ts.SyntaxKind.LastAssignment + ) + return undefined; + let target = node.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (isCjsModuleExports(lowerer, target)) out.moduleExports = true; + if (isCjsExportsIdentifier(lowerer, target)) out.exports = true; + return undefined; + }); + return out; +} + +/** Whether `node` still names the module's ORIGINAL exports object wherever + * it runs: `exports` unless the file rebinds it; `module.exports` unless the + * file rebinds `exports` or replaces `module.exports`. */ +function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean { + const assigned = exportsAssignments(lowerer, node.getSourceFile()); + if (assigned.exports) return false; + return ts.isIdentifier(node) || !assigned.moduleExports; +} + +/** The uses this module answers with the original exports object: identity + * comparisons and `typeof` of either spelling, and truthiness or `void` of + * `exports` (`module.exports` truthiness keeps its existing fold). */ +function objectAnsweredUse(node: ts.Expression): boolean { + if (identityUse(node)) return true; + let parent = node.parent; + while (parent !== undefined && ts.isParenthesizedExpression(parent)) parent = parent.parent; + if (parent !== undefined && ts.isTypeOfExpression(parent)) return true; + return ts.isIdentifier(node) && answeredWithoutObject(node); +} + +/** Whether `node` (`exports` or `module.exports`) is a use of the module's + * exports object as a VALUE (not a member access receiver, not an + * assignment target). */ +export function isCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): boolean { + if (!isCjsExportsIdentifier(lowerer, node) && !isCjsModuleExports(lowerer, node)) return false; + let outer: ts.Node = node; + while (outer.parent !== undefined && ts.isParenthesizedExpression(outer.parent)) + outer = outer.parent; + const parent = outer.parent; + if (parent === undefined) return false; + if ( + (ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) && + parent.expression === outer + ) + return false; + if ( + ts.isBinaryExpression(parent) && + parent.left === outer && + parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) + return false; + return true; +} + +/** Files whose exports object is observed by identity somewhere: their top- + * level `this` object must exist (lowerFileInit creates it). */ +export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boolean { + if (!isCjsJsFile(sf, lowerer.program)) return false; + let found = false; + ts.walkPreorder(sf, (node) => { + if ( + (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) && + isCjsExportsObjectValue(lowerer, node) && + objectAnsweredUse(node) && + namesOriginalExports(lowerer, node) + ) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** `exports` / `module.exports` used as a value inside its own module: the + * original exports object for identity, typeof and truthiness, or a named + * refusal. Null when `node` is not such a use. */ +export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null { + if (!isCjsExportsObjectValue(lowerer, node)) return null; + const sf = node.getSourceFile(); + // A module that replaces module.exports answers `module.exports` reads + // through the replacement's own machinery (the class-expression whole + // export, the replaced-exports reads). + if (!ts.isIdentifier(node) && exportsAssignments(lowerer, sf).moduleExports) return null; + const spelled = ts.isIdentifier(node) ? "exports" : "module.exports"; + const original = lowerer.cjsExportsThis.get(sf); + if (objectAnsweredUse(node) && original !== undefined && namesOriginalExports(lowerer, node)) { + return { kind: "varRef", localId: original.id, type: DYN, loc: locOf(node) }; + } + // `module.exports` truthiness keeps its existing fold (and anything the + // object is not needed for keeps whatever answered it before). + if (answeredWithoutObject(node)) return null; + lowerer.unsupported( + "SC1090", + node, + identityUse(node) + ? `comparing '${spelled}' in a module that replaces module.exports or rebinds exports (the compiled exports are a static table, not an object with an identity)` + : `'${spelled}' as a value other than a member access, comparison, typeof or truthiness test (the compiled exports are a static table, not an object: read ${spelled}.)`, + ); +} + +/** `exports = module.exports = {}` (either order) as a top-level statement + * that runs before anything could observe or extend the original exports + * object: only directives, require statements and function declarations + * precede it, and the module assigns neither binding anywhere else. Node + * swaps the original empty exports object for another empty one that both + * bindings name; the compiled module's static exports start out exactly + * the same, so the statement is a no-op (semver's internal/re.js). */ +export function isFreshExportsReset(lowerer: Lowerer, stmt: ts.ExpressionStatement): boolean { + const sf = stmt.parent; + if (!ts.isSourceFile(sf) || !isCjsJsFile(sf, lowerer.program)) return false; + let e: ts.Expression = stmt.expression; + const targets: ts.Expression[] = []; + for (;;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + targets.push(e.left); + e = e.right; + } + if (!ts.isObjectLiteralExpression(e) || e.properties.length !== 0) return false; + if (targets.length !== 2) return false; + const hasExports = targets.some((t) => isCjsExportsIdentifier(lowerer, t)); + const hasModuleExports = targets.some((t) => isCjsModuleExports(lowerer, t)); + if (!hasExports || !hasModuleExports) return false; + for (const prior of sf.statements) { + if (prior === stmt) break; + if (ts.isFunctionDeclaration(prior) || isRequireStatement(prior)) continue; + if (ts.isExpressionStatement(prior) && ts.isStringLiteral(prior.expression)) continue; + return false; + } + let others = 0; + ts.walkPreorder(sf, (node) => { + if ( + !ts.isBinaryExpression(node) || + node.operatorToken.kind < ts.SyntaxKind.FirstAssignment || + node.operatorToken.kind > ts.SyntaxKind.LastAssignment + ) + return undefined; + let target = node.left; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (isCjsModuleExports(lowerer, target) || isCjsExportsIdentifier(lowerer, target)) others++; + return undefined; + }); + return others === 2; +} + +/** The export global a WRITE `exports. = v` / `module.exports. = + * v` updates when it is not a top-level export statement (inside a + * function, or in value position): only while the module keeps its + * original exports object, which is then the object every requirer reads + * the member from. */ +export function cjsExportMemberWriteGlobalOf( + lowerer: Lowerer, + target: ts.PropertyAccessExpression, +): IrGlobal | null { + const sf = target.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + const assigned = exportsAssignments(lowerer, sf); + if (assigned.exports || assigned.moduleExports) return null; + return cjsInFileExportGlobalOf(lowerer, target); +} + +/** Whether a function body reads `this` (arrow functions inherit it; other + * nested functions bind their own). */ +function functionUsesThis(fn: ts.FunctionLikeDeclaration): boolean { + if (fn.body === undefined) return false; + let found = false; + ts.walkPreorder(fn.body, (node) => { + if (node !== fn.body && ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip"; + if (node.kind === ts.SyntaxKind.ThisKeyword) { + found = true; + return "stop"; + } + return undefined; + }); + return found; +} + +/** A function declaration, or a variable initialized with a function or + * arrow expression, that never reads `this`. */ +function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined): boolean { + const decls = sym ? lowerer.checker.declarationsOf(sym) : []; + const fn = decls.find(ts.isFunctionDeclaration); + if (fn !== undefined) return !functionUsesThis(fn); + const v = decls.find(ts.isVariableDeclaration); + let init = v?.initializer; + while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression; + return ( + init !== undefined && + (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) && + !functionUsesThis(init) + ); +} + +/** True when every value the module assigns to its export `name` is a + * function that never reads `this`. A call through the exports object + * (`exports.f(x)`) binds `this` to that object, which the static export + * table cannot supply, so only these calls lower; the others keep a fence. */ +export function cjsExportCallIgnoresThis( + lowerer: Lowerer, + sf: ts.SourceFile, + name: string, + depth = 0, +): boolean { + if (depth > 16) return false; + const values: ts.Expression[] = []; + ts.walkPreorder(sf, (node) => { + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(node.left) && + node.left.name.text === name && + (isCjsExportsIdentifier(lowerer, node.left.expression) || + isCjsModuleExports(lowerer, node.left.expression)) + ) { + values.push(node.right); + } + return undefined; + }); + // Entries of a `module.exports = { ... }` table (direct, or a const + // literal the statement names). + let methodEntry: ts.MethodDeclaration | null = null; + let shorthandOnly = false; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind !== "table") continue; + const table = cjs.obj ?? cjsExportTargetLiteral(cjs.expr.right, sf); + for (const prop of table?.properties ?? []) { + if (prop.name === undefined || !ts.isIdentifier(prop.name) || prop.name.text !== name) + continue; + if (ts.isPropertyAssignment(prop)) values.push(prop.initializer); + else if (ts.isShorthandPropertyAssignment(prop)) { + const value = lowerer.checker.getShorthandAssignmentValueSymbol(prop); + if (!symbolIsThisFreeFunction(lowerer, value)) return false; + shorthandOnly = true; + } else if (ts.isMethodDeclaration(prop)) methodEntry = prop; + } + } + if (methodEntry !== null) return values.length === 0 && !functionUsesThis(methodEntry); + if (values.length === 0) return shorthandOnly; + return values.every((v) => { + let e = v; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return !functionUsesThis(e); + if (ts.isIdentifier(e)) { + return symbolIsThisFreeFunction(lowerer, lowerer.checker.getSymbolAtLocation(e)); + } + // Another module's member (`exports.f = require("./lib").f`, or through + // a module binding): that module's own export values decide. + if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.name)) { + const dep = ts.isIdentifier(e.expression) + ? lowerer.cjsLocalModuleBindingOf(e.expression) + ? lowerer.cjsModuleBindingDepOf(e.expression) + : null + : lowerer.cjsModuleRefDepOf(e.expression); + return dep !== null && cjsExportCallIgnoresThis(lowerer, dep, e.name.text, depth + 1); + } + return false; + }); +} diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts new file mode 100644 index 0000000000..75474565b3 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -0,0 +1,371 @@ +/* Inline CommonJS `require("./x")` of a program module in expression + * position. A top-level `const m = require("./x")` binding is alias + * plumbing: member reads resolve to the exporter's declarations, and the + * statement lowers to the dependency's run-once %init call. An inline + * require is the same thing without the binding: the %init call runs at + * exactly the call's position (Node evaluates the module there, or + * answers from its cache), and the value is then read statically: + * + * - `require("./x").f` and `require("./x").f(...)` over a module whose + * exports are a table (member exports or `module.exports = {...}`) + * read the member like a binding would; + * - `require("./x")` as a value over a module whose whole export is one + * value (`module.exports = fn` / `= class {}` / `= make()` / `= 42` / + * `= require("./y")`) answers that value. + * + * A table module's exports object as a value has no static + * representation; it keeps a named refusal (a first-class CommonJS module + * object is the two-tier design's S9 stage). */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + canonicalBuiltinModule, + cjsExportAssignmentOf, + cjsExportDiscardReason, + cjsExportTargetLiteral, + isCjsJsFile, + isJsSourceFile, + isInlineRequireCycleCall, + isModuleExportsAccess, + locOf, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; +import { isCjsRequireGlobal } from "./lower-node-module.js"; +import { DYN, type IrExpr } from "../../ir/ir.js"; +import { nodeThrowExpr } from "../../ir/build.js"; +import { probeNodeRequireRefusal } from "../npm.js"; + +export interface InlineRequire { + call: ts.CallExpression; + spec: string; + dep: ts.SourceFile; +} + +/** A `require("")` 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 + * createRequire bindings answer null: their own lowerings own them. */ +export function inlineProgramRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null; + const spec = requireSpecOf(e); + if (spec === null || canonicalBuiltinModule(spec) !== null) return null; + const sf = e.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (!isCjsRequireGlobal(lowerer, e.expression)) return null; + if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || dep.fileName.endsWith(".json")) return null; + return { call: e, spec, dep }; +} + +/** The kept (last, not discarded) top-level `module.exports = ` of a + * CommonJS module whose whole export is a single value, or null when its + * exports are a table: member exports only, or a literal / const-literal / + * Proxy-over-literal table. */ +export function cjsWholeExportValueOf( + sf: ts.SourceFile, +): { assign: ts.BinaryExpression; rhs: ts.Expression } | null { + let kept: ts.BinaryExpression | null = null; + for (const stmt of sf.statements) { + const cjs = cjsExportAssignmentOf(stmt); + if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) kept = cjs.expr; + } + if (kept === null) return null; + let rhs: ts.Expression = kept.right; + while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + if (ts.isObjectLiteralExpression(rhs) || cjsExportTargetLiteral(rhs, sf) !== null) return null; + return { assign: kept, rhs }; +} + +/** True when `dep`'s exports, through `module.exports = require("./y")` + * re-export chains, are a table that member reads resolve against. */ +export function cjsNamespaceModule(lowerer: Lowerer, dep: ts.SourceFile, depth = 0): boolean { + const whole = cjsWholeExportValueOf(dep); + if (whole === null) return true; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + return inner !== null && depth < 32 && cjsNamespaceModule(lowerer, inner.dep, depth + 1); +} + +/** An inline program-module require over a table module (member reads and + * member calls resolve through it), or null. */ +export function inlineNamespaceRequireOf( + lowerer: Lowerer, + expr: ts.Expression, +): InlineRequire | null { + const req = inlineProgramRequireOf(lowerer, expr); + return req !== null && cjsNamespaceModule(lowerer, req.dep) ? req : null; +} + +/** `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 (isInlineRequireCycleCall(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)) { + lowerer.unsupported( + "SC1090", + req.call, + `require() of '${req.spec}' here (the module is not part of the compiled module order)`, + ); + } + const init = lowerer.requireInitStmt(req.spec, req.call); + if (init === null) return value; + return { kind: "seqExpr", stmts: [init], result: value, type: value.type, loc: value.loc }; +} + +/** The whole-export value of `dep` read from the requirer, following + * `module.exports = require("./y")` re-exports (the inner module already + * evaluated inside dep's own %init). Null for table modules and for + * whole-export shapes with no represented value. */ +export function wholeExportValue( + lowerer: Lowerer, + dep: ts.SourceFile, + blame: ts.Node, + depth = 0, +): IrExpr | null { + const whole = cjsWholeExportValueOf(dep); + if (whole === null || depth > 32) return null; + const inner = inlineProgramRequireOf(lowerer, whole.rhs); + if (inner !== null) return wholeExportValue(lowerer, inner.dep, blame, depth + 1); + // An identifier names a declaration of the exporting module (or a + // builtin global): reading it IS the export, exactly what requirer + // bindings alias to. + if (ts.isIdentifier(whole.rhs)) return lowerer.lowerExpr(whole.rhs); + if (ts.isClassExpression(whole.rhs)) { + return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.rhs), blame); + } + const g = lowerer.globalsByDeclNode.get(whole.assign); + if (g === undefined) return null; + return { kind: "varRef", localId: g.id, type: g.type, loc: locOf(blame) }; +} + +/** `require("./x")` as a value (`require("debug")("ns")`, `new + * (require("./C"))()`, `{ verify: require("./verify") }`): the module's + * %init at this position, then its whole-export value. Null when `call` + * is not an inline program-module require. */ +export function lowerInlineRequireValue(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const req = inlineProgramRequireOf(lowerer, call); + if (req === null) return null; + const value = wholeExportValue(lowerer, req.dep, call, 0); + if (value === null) { + lowerer.unsupported( + "SC1090", + call, + cjsNamespaceModule(lowerer, req.dep) + ? `the exports object of '${req.spec}' as a value (its exports compile to a static table: read require(${JSON.stringify(req.spec)}).member directly, or bind const m = require(${JSON.stringify(req.spec)}) at the module's top level)` + : `the whole export of '${req.spec}' as a value (its 'module.exports =' value has no static representation)`, + ); + } + return withRequireInit(lowerer, req, value); +} + +/** Lowers `inner` (a member read or member call whose receiver is the + * inline require) with that require marked as handled, so the receiver + * resolves as a module binding, then sequences the module's %init first. */ +export function lowerWithInlineRequireInit( + lowerer: Lowerer, + req: InlineRequire, + inner: () => IrExpr, +): IrExpr { + lowerer.inlineRequireInitDone.add(req.call); + try { + return withRequireInit(lowerer, req, inner()); + } finally { + lowerer.inlineRequireInitDone.delete(req.call); + } +} + +/** A `require(...)` call of the CommonJS global in a CommonJS file that no + * other lowering claimed (not a builtin, JSON document, program module or + * createRequire binding): a package nothing installed resolves throws + * Node's catchable MODULE_NOT_FOUND at the call, and every other form is + * refused by name instead of as an unlowerable `require` value. */ +export function lowerUncompiledRequireCall( + lowerer: Lowerer, + call: ts.CallExpression, +): IrExpr | null { + if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null; + const sf = call.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression)) + return null; + if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; + const spec = requireSpecOf(call); + if (spec === null) { + lowerer.unsupported( + "SC1090", + call, + 'require() with a computed specifier (the compiled module graph is fixed at build time: require("") lowers)', + ); + } + if (canonicalBuiltinModule(spec) !== null) return null; + if (!spec.startsWith(".") && !spec.startsWith("/") && !spec.startsWith("#")) { + const refusal = probeNodeRequireRefusal(sf.fileName, spec); + if (refusal !== null) { + return nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, DYN, locOf(call)); + } + lowerer.unsupported( + "SC1090", + call, + `require() of the package '${spec}' in an expression (the package is not compiled into this program)`, + ); + } + lowerer.unsupported( + "SC1090", + call, + `require() of '${spec}' in an expression (it does not resolve to a compiled program module)`, + ); +} + +/** The elements of a top-level `const { key: { ... } } = require("./x")` + * declaration over a program module whose nested pattern destructures the + * named export's value (preflight admits exactly these). Plain elements + * stay alias plumbing. */ +export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.BindingElement[] { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return []; + const spec = requireSpecOf(decl.initializer); + if (spec === null || canonicalBuiltinModule(spec) !== null) return []; + if (!isJsSourceFile(decl.getSourceFile())) return []; + return decl.name.elements.filter( + (e) => + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) && + e.initializer === undefined && + e.dotDotDotToken === undefined, + ); +} + +/** A plain method entry of a `module.exports = { ... }` table: identifier + * name, a body, not a generator. Its value is the method's function. */ +export function cjsTableMethodEntry( + prop: ts.ObjectLiteralElementLike, +): (ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }) | null { + if (!ts.isMethodDeclaration(prop) || !ts.isIdentifier(prop.name) || prop.body === undefined) + return null; + if (prop.asteriskToken !== undefined) return null; + return prop as ts.MethodDeclaration & { name: ts.Identifier; body: ts.Block }; +} + +/** `exports. = value` / `module.exports. = value` assignments in + * VALUE position inside a top-level statement (`const src = exports.src = + * []`), outside function bodies and outside the statement's own export + * chain (`statementExport`, which the statement-level machinery owns). */ +export function nestedCjsExportAssignmentsOf( + stmt: ts.Statement, + statementExport: ts.BinaryExpression | undefined, +): { name: ts.Identifier; value: ts.Expression }[] { + if (!ts.isVariableStatement(stmt) && !ts.isExpressionStatement(stmt)) return []; + const owned = new Set(); + for (let e: ts.Expression | undefined = statementExport; e !== undefined;) { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + owned.add(e); + e = e.right; + } + const out: { name: ts.Identifier; value: ts.Expression }[] = []; + ts.walkPreorder(stmt, (node) => { + if ( + node !== stmt && + (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + ) + return "skip"; + if ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + !owned.has(node) && + ts.isPropertyAccessExpression(node.left) && + node.left.questionDotToken === undefined && + ts.isIdentifier(node.left.name) && + ((ts.isIdentifier(node.left.expression) && node.left.expression.text === "exports") || + isModuleExportsAccess(node.left.expression)) + ) { + out.push({ name: node.left.name, value: node.right }); + } + return undefined; + }); + return out; +} + +/** `const { A, B: b } = ns` at a module's top level, where `ns` is a + * CommonJS module binding over a table module (picomatch's `const { + * MAX_LENGTH, ... } = constants`): each name takes a snapshot of the named + * export's value at this statement, exactly like destructuring the exports + * object would. Null for every other declaration; the plain elements and + * their key names otherwise. */ +export function cjsNamespaceDestructureOf( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): { dep: ts.SourceFile; elements: { name: ts.Identifier; key: string }[] } | null { + if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + if (!isJsSourceFile(stmt.parent)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isIdentifier(init) || !lowerer.cjsLocalModuleBindingOf(init)) return null; + const dep = lowerer.cjsModuleBindingDepOf(init); + if (dep === null) return null; + const elements: { name: ts.Identifier; key: string }[] = []; + for (const e of decl.name.elements) { + if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return null; + if (e.name === undefined || !ts.isIdentifier(e.name)) return null; + const key = + e.propertyName === undefined + ? e.name.text + : ts.isIdentifier(e.propertyName) || ts.isStringLiteral(e.propertyName) + ? e.propertyName.text + : null; + if (key === null) return null; + elements.push({ name: e.name, key }); + } + return { dep, elements }; +} + +/** The value of `dep`'s export `key` read from another module: its export + * global, or the function/class declaration it aliases. Null when it has no + * static value. */ +export function cjsExportValueByName( + lowerer: Lowerer, + dep: ts.SourceFile, + key: string, + blame: ts.Node, +): IrExpr | null { + let sym = lowerer.cjsModuleExportSymbol(dep, key); + if (sym === undefined) return null; + if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym); + const direct = lowerer.globalsBySymbol.get(sym); + if (direct) return { kind: "varRef", localId: direct.id, type: direct.type, loc: locOf(blame) }; + for (const d of lowerer.checker.declarationsOf(sym)) { + const byDecl = lowerer.globalsByDeclNode.get(d); + if (byDecl) return { kind: "varRef", localId: byDecl.id, type: byDecl.type, loc: locOf(blame) }; + // Alias plumbing: a shorthand/identifier table entry or a member + // export naming a declaration. Reading that identifier IS the value. + if (ts.isShorthandPropertyAssignment(d)) return lowerer.lowerShorthandValue(d); + if (ts.isPropertyAssignment(d) && ts.isIdentifier(d.initializer)) { + return lowerer.lowerExpr(d.initializer); + } + if ((ts.isFunctionDeclaration(d) || ts.isClassDeclaration(d)) && d.name !== undefined) { + return lowerer.lowerExpr(d.name); + } + } + return null; +} diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts new file mode 100644 index 0000000000..f7955fc239 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts @@ -0,0 +1,223 @@ +/* tsc's CommonJS import interop: `const x_1 = __importDefault(require("./m"))` + * and `const x_1 = __importStar(require("./m"))` at a module's top level, + * with the helpers tsc emits at the top of the file: + * + * var __importDefault = (this && this.__importDefault) || function (mod) { + * return (mod && mod.__esModule) ? mod : { "default": mod }; + * }; + * + * For a CommonJS dependency the helper's answer is fixed by the + * dependency's source: stamped with `__esModule` (tsc output), the binding + * IS the module's exports; otherwise `x_1.default` is the module's exports + * (and __importStar also copies every export onto x_1). Both are the + * static alias model a plain `const x_1 = require("./m")` already has, so + * the binding lowers as a require binding: the statement is the module's + * %init, `x_1.f` / `x_1.default.f` read the exports statically, and + * `x_1.default` over a single-value module is that value. The helper + * declarations themselves are dead once every call is such a binding (the + * self-probe `this.__importDefault` reads the pristine exports object, + * which never has the name), so they lower to nothing. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + isCjsJsFile, + isEsModuleStamp, + npmStaticDepSf7, + requireSpecOf, + resolveImport, +} from "../program.js"; +import { cjsNamespaceModule } from "./cjs-inline-require.js"; + +const HELPERS = new Set([ + "__importDefault", + "__importStar", + "__createBinding", + "__setModuleDefault", +]); + +export interface TscInteropBinding { + helper: "__importDefault" | "__importStar"; + spec: string; + dep: ts.SourceFile; + /** The dependency stamps `__esModule`: the binding IS its exports. */ + stamped: boolean; +} + +/** `var = (this && this.) || ` at a module's top + * level: the tsc helper declaration shape. */ +function tscHelperDeclName(stmt: ts.Statement): string | null { + if (!ts.isVariableStatement(stmt)) return null; + const decls = stmt.declarationList.declarations; + if (decls.length !== 1) return null; + const d = decls[0]!; + if (!ts.isIdentifier(d.name) || !HELPERS.has(d.name.text) || d.initializer === undefined) + return null; + let init: ts.Expression = d.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.BarBarToken) + return null; + let probe: ts.Expression = init.left; + while (ts.isParenthesizedExpression(probe)) probe = probe.expression; + if ( + !ts.isBinaryExpression(probe) || + probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken || + probe.left.kind !== ts.SyntaxKind.ThisKeyword || + !ts.isPropertyAccessExpression(probe.right) || + probe.right.expression.kind !== ts.SyntaxKind.ThisKeyword || + probe.right.name.text !== d.name.text + ) + return null; + // The fallback is the helper body: a function, or (newer tsc output) a + // conditional or IIFE producing one. It never runs when the helper is dead. + return d.name.text; +} + +/** True when the dependency stamps itself as an ES module translation. */ +function stampsEsModule(dep: ts.SourceFile): boolean { + return dep.statements.some((s) => { + if (!ts.isExpressionStatement(s)) return false; + if (isEsModuleStamp(s.expression)) return true; + const e = s.expression; + return ( + ts.isBinaryExpression(e) && + e.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(e.left) && + ts.isIdentifier(e.left.expression) && + e.left.expression.text === "exports" && + e.left.name.text === "__esModule" && + e.right.kind === ts.SyntaxKind.TrueKeyword + ); + }); +} + +/** The interop binding a top-level `const x = __importDefault(require(s))` / + * `__importStar(require(s))` declaration makes, or null. The helper must be + * the module's own tsc helper declaration and the dependency a compiled + * CommonJS program module. */ +export function tscInteropBindingOfDecl( + lowerer: Lowerer, + decl: ts.VariableDeclaration, +): TscInteropBinding | null { + if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null; + const stmt = decl.parent?.parent; + if (stmt === undefined || !ts.isVariableStatement(stmt) || !ts.isSourceFile(stmt.parent)) + return null; + const sf = stmt.parent; + if (!isCjsJsFile(sf, lowerer.program)) return null; + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (!ts.isCallExpression(init) || init.arguments.length !== 1) return null; + const callee = init.expression; + if (!ts.isIdentifier(callee)) return null; + if (callee.text !== "__importDefault" && callee.text !== "__importStar") return null; + if (!sf.statements.some((s) => tscHelperDeclName(s) === callee.text)) return null; + const spec = requireSpecOf(init.arguments[0]!); + if (spec === null) return null; + const dep = + resolveImport(lowerer.program, sf, spec, "require") ?? + npmStaticDepSf7(lowerer.program, sf, spec, "require"); + if (!dep || dep.isDeclarationFile || !isCjsJsFile(dep, lowerer.program)) return null; + return { + helper: callee.text as TscInteropBinding["helper"], + spec, + dep, + stamped: stampsEsModule(dep), + }; +} + +/** The interop binding an identifier reads, or null. */ +export function tscInteropBindingOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + if (!ts.isIdentifier(expr)) return null; + const sym = lowerer.checker.getSymbolAtLocation(expr); + const decl = sym ? lowerer.checker.declarationsOf(sym).find(ts.isVariableDeclaration) : undefined; + return decl ? tscInteropBindingOfDecl(lowerer, decl) : null; +} + +/** The module whose exports `expr` denotes through an interop binding: + * the binding itself when the dependency is stamped (or for __importStar, + * whose copy carries every export), and `binding.default` when it is not. + * Null for anything else. */ +export function tscInteropModuleOf( + lowerer: Lowerer, + expr: ts.Expression, +): TscInteropBinding | null { + let e = expr; + while (ts.isParenthesizedExpression(e)) e = e.expression; + const direct = tscInteropBindingOf(lowerer, e); + if (direct !== null) { + return direct.stamped || direct.helper === "__importStar" ? direct : null; + } + if (ts.isPropertyAccessExpression(e) && !e.questionDotToken && e.name.text === "default") { + const via = tscInteropBindingOf(lowerer, e.expression); + if (via !== null && !via.stamped) return via; + } + return null; +} + +/** `binding.default` over an unstamped single-value dependency: that value + * (the caller lowers the dependency's whole export). */ +export function tscInteropDefaultValueOf( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): TscInteropBinding | null { + if (expr.questionDotToken || expr.name.text !== "default") return null; + const via = tscInteropBindingOf(lowerer, expr.expression); + if (via === null || via.stamped) return null; + return cjsNamespaceModule(lowerer, via.dep) ? null : via; +} + +/** True for a tsc helper declaration every reference to which is a + * recognized interop binding's callee, its own self-probe, or a reference + * from another such dead helper: the statement lowers to nothing. */ +export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean { + const name = tscHelperDeclName(stmt); + if (name === null) return false; + const sf = stmt.getSourceFile(); + const helperSpans = new Map(); + for (const s of sf.statements) { + const n = tscHelperDeclName(s); + if (n !== null) helperSpans.set(n, s); + } + const dead = new Set(); + for (let changed = true; changed;) { + changed = false; + for (const [helper, decl] of helperSpans) { + if (dead.has(helper)) continue; + let live = false; + ts.walkPreorder(sf, (node) => { + if (!ts.isIdentifier(node) || node.text !== helper) return undefined; + const parent = node.parent; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node) + return undefined; + const at = node.getStart(sf); + if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined; + for (const [other, otherDecl] of helperSpans) { + if (other !== helper && dead.has(other)) { + if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined; + } + } + const call = parent; + const binding = call?.parent; + if ( + call !== undefined && + ts.isCallExpression(call) && + call.expression === node && + binding !== undefined && + ts.isVariableDeclaration(binding) && + tscInteropBindingOfDecl(lowerer, binding) !== null + ) + return undefined; + live = true; + return "stop"; + }); + if (!live) { + dead.add(helper); + changed = true; + } + } + } + return dead.has(name); +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2037ba17c1..213e605bcb 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -24,6 +24,13 @@ import { lowerZlibStreamMethodCall, } from "./builtins/compression.js"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerInlineRequireValue, + lowerUncompiledRequireCall, + lowerWithInlineRequireInit, +} from "./cjs-inline-require.js"; +import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; import { errorToStringCall } from "./error-methods.js"; @@ -234,6 +241,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const json = lowerJsonRequireCall(lowerer, expr); if (json) return json; + // Inline require() of a program module (cjs-inline-require.ts): a + // member call over its export table resolves like a binding's member + // call after the module's %init at this position; the require itself + // as a value answers the module's whole-export value. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + // Node calls the member with `this` bound to the module's exports. + if ( + ts.isIdentifier(expr.expression.name) && + !cjsExportCallIgnoresThis(lowerer, req.dep, expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.name.text}' through require(${JSON.stringify(req.spec)}) when the function may read 'this' (Node binds it to the module's exports object, which compiles to a static table — bind the module and call a local function instead)`, + ); + } + return lowerWithInlineRequireInit(lowerer, req, () => lowerCall(lowerer, expr)); + } + } + { + const inlineValue = lowerInlineRequireValue(lowerer, expr); + if (inlineValue) return inlineValue; + } + // A call whose chain ROOTS at an ambient-undefined name (`declare // const value: Y | undefined; value?.foo("a")`, `declare function // chain...; chain(o).mapValues(f).value()`, a trap binding's read): @@ -310,6 +343,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerImportMetaResolveCall(lowerer, expr) ?? lowerRequireResolveCall(lowerer, expr); if (resolved) return resolved; } + // Any other require() call of the CommonJS global: MODULE_NOT_FOUND for + // packages nothing installed resolves, a named refusal otherwise. + { + const uncompiled = lowerUncompiledRequireCall(lowerer, expr); + if (uncompiled) return uncompiled; + } // `process.getuid?.()` — intercepted BEFORE the optional-chain // machinery (the member always exists on a POSIX target, so the @@ -1274,6 +1313,32 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); return { kind: "callValue", callee, args, type: callee.type.ret, loc }; } + // `exports.f(x)` / `module.exports.f(x)` inside the module: a call of + // the member's export global (cjs-exports-object.ts), the value every + // `exports.f =` statement assigns. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.questionDotToken) { + const g = cjsInFileExportGlobalOf(lowerer, expr.expression); + if ( + g && + (g.type.kind === "func" || g.type.kind === "dyn") && + !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text) + ) { + lowerer.unsupported( + "SC1090", + expr, + `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`, + ); + } + if (g && (g.type.kind === "func" || g.type.kind === "dyn")) { + const callee: IrExpr = { kind: "varRef", localId: g.id, type: g.type, loc }; + const checked = lowerCheckedCallableValue(lowerer, expr, callee, loc); + if (checked) return checked; + if (callee.type.kind === "func") { + const args = completeFuncValueArgs(lowerer, expr, callee.type, loc); + return { kind: "callValue", callee, args, type: callee.type.ret, loc }; + } + } + } if (ts.isPropertyAccessExpression(expr.expression)) { const intrinsic = fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ?? diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index fcc54b32b7..a18fb83a3b 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -13,6 +13,7 @@ import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js"; import * as ts from "../ts7/adapter.js"; import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; +import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js"; import { BOOL, DATE_T, @@ -8445,6 +8446,14 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const loc = locOf(expr); + // `new (require("./x").C)()`: the module's %init at the require's + // position, then construction through the member as for a binding. + if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerNew(lowerer, expr)); + } + } const worker = lowerWorkerNew(lowerer, expr); if (worker) return worker; const readlineUi = lowerReadlineUiNew(lowerer, expr); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index d4d03591da..a50b2ee008 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -43,6 +43,18 @@ import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; import type { Lowerer } from "./lowerer.js"; +import { + inlineNamespaceRequireOf, + lowerWithInlineRequireInit, + wholeExportValue, +} from "./cjs-inline-require.js"; +import { tscInteropDefaultValueOf } from "./cjs-tsc-interop.js"; +import { + cjsExportMemberWriteGlobalOf, + cjsInFileExportGlobalOf, + isCjsExportsIdentifier, + lowerCjsExportsObjectValue, +} from "./cjs-exports-object.js"; import { checkedClassAssertion, checkedClassUnionAssertion } from "./class-assertions.js"; import { checkedArmExtraction } from "./coercions/unions.js"; import { narrowClassUnion, narrowStoredClassValue } from "./class-unions.js"; @@ -205,6 +217,7 @@ import { isNodeModuleValue, lowerCjsTopLevelThis, lowerNodeModuleIdentifier, + isCjsModuleGlobal, lowerNodeModuleProperty, lowerRequireCacheElement, lowerRequireCacheHas, @@ -858,6 +871,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { const moduleValue = lowerNodeModuleIdentifier(lowerer, expr); if (moduleValue) return moduleValue; + if (expr.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } if (lowerer.isSelfReference(expr)) { return { kind: "selfRef", type: lowerer.ctx.selfType!, loc }; } @@ -1836,6 +1853,28 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); if (ts.isPropertyAccessExpression(expr)) { + // `require("./x").member` over a program module's export table: the + // module's %init at this position, then the member read exactly like + // a `const m = require("./x")` binding's (cjs-inline-require.ts). + if (!expr.questionDotToken) { + const req = inlineNamespaceRequireOf(lowerer, expr.expression); + if (req !== null && !lowerer.inlineRequireInitDone.has(req.call)) { + return lowerWithInlineRequireInit(lowerer, req, () => lowerExpr(lowerer, expr)); + } + // `module.exports` compared by identity inside its own module, or + // refused by name as any other value (cjs-exports-object.ts). + if (expr.name.text === "exports") { + const exportsValue = lowerCjsExportsObjectValue(lowerer, expr); + if (exportsValue) return exportsValue; + } + // tsc's `x_1.default` over a dependency whose whole export is one + // value: that value (cjs-tsc-interop.ts). + const interop = tscInteropDefaultValueOf(lowerer, expr); + if (interop !== null) { + const value = wholeExportValue(lowerer, interop.dep, expr); + if (value !== null) return value; + } + } { const member = lifetimeRegionMemberOf(lowerer.program, expr, lowerer.experimentalFiles); if (member !== null) @@ -2181,35 +2220,12 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { // global (pure alias plumbing) and fall through unchanged; write // positions belong to the export-assignment machinery. { - const sf = expr.getSourceFile(); - const cjsMemberRecv = - isModuleExportsAccess(expr.expression) || - (ts.isIdentifier(expr.expression) && - expr.expression.text === "exports" && - !lowerer.resolveLocal(expr.expression) && - !lowerer.globalOf(expr.expression)); const writePos = ts.isBinaryExpression(expr.parent) && expr.parent.left === expr && expr.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken; - if ( - !expr.questionDotToken && - cjsMemberRecv && - !writePos && - isCjsJsFile(sf, lowerer.program) - ) { - const sym = - lowerer.checker.getSymbolAtLocation(expr.name) ?? - lowerer.cjsModuleExportSymbol(sf, expr.name.text); - const resolved = - sym !== undefined && sym.flags & ts.SymbolFlags.Alias - ? lowerer.checker.getAliasedSymbol(sym) - : sym; - const g = - (sym ? lowerer.globalsBySymbol.get(sym) : undefined) ?? - (resolved ? lowerer.globalsBySymbol.get(resolved) : undefined); - if (g) return { kind: "varRef", localId: g.id, type: g.type, loc }; - } + const g = cjsInFileExportGlobalOf(lowerer, expr); + if (g && !writePos) return { kind: "varRef", localId: g.id, type: g.type, loc }; } // Namespace-qualified reads (`N.x`, `A.B.C.f`, import= alias // chains): the member is a compile-time-known declaration of a @@ -2805,6 +2821,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { ); } } + // A member a JavaScript module attached to one of its functions + // (`status.message = codes; module.exports = status`), read from + // TypeScript: JavaScript stores such members on the function's own + // property table (`fn.member = v` writes through the function's + // checked-dynamic face), so the read takes the same path and enters + // the static code at the type inference gave the member. + if (recvLowered.type.kind === "func" && jsFunctionExpandoMember(lowerer, expr)) { + const read: IrExpr = { + kind: "dynKeyGet", + value: lowerer.coerceToExpected(recvLowered, DYN), + key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) }, + type: DYN, + loc, + }; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr)); + return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read; + } lowerer.unsupported( "SC1090", expr, @@ -10151,6 +10184,35 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr // RHS value is the expression's value — its OWN type, so the // consumer sees exactly what the checker typed. if (ts.isPropertyAccessExpression(expr.left) && !expr.left.questionDotToken) { + // `const src = exports.src = []`: an export member assigned in + // value position writes its export global and yields the value. + { + let inFunction = false; + for (let p: ts.Node | undefined = expr.parent; p !== undefined; p = p.parent) { + if (ts.isFunctionLike(p) || ts.isClassDeclaration(p) || ts.isClassExpression(p)) { + inFunction = true; + break; + } + } + const g = inFunction + ? cjsExportMemberWriteGlobalOf(lowerer, expr.left) + : cjsInFileExportGlobalOf(lowerer, expr.left); + if (g) { + const value = lowerer.lowerExprExpecting(expr.right, g.type); + const tmp = lowerer.declareHiddenLocal("%exportValue", value.type); + const result = varRef(tmp.id, value.type, loc); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: tmp.id, init: value, loc }, + { kind: "assign", localId: g.id, value: result, loc }, + ], + result, + type: value.type, + loc, + }; + } + } const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") return lowerDynMemberAssignment(lowerer, expr, recv); const unionWrite = lowerUnionFieldWrite(lowerer, expr.left, expr.right); @@ -14977,6 +15039,9 @@ export function fieldTarget( access: ts.PropertyAccessExpression, ): FieldTarget | null { if (lowerer.cjsLocalModuleBindingOf(access.expression)) return null; + // `module.exports` / `module.id`: CommonJS module surface, never a field. + if (isCjsModuleGlobal(lowerer, access.expression)) return null; + if (isCjsExportsIdentifier(lowerer, access.expression)) return null; if (lowerer.chainBlocked(access)) return null; if ( access.name.text === "stackTraceLimit" && @@ -16263,3 +16328,28 @@ function isBuiltinModuleValueDecl(decl: ts.Node): boolean { ts.isVariableDeclaration(decl)) ); } + +/** True when `access` reads a member that JavaScript sources attach to a + * function by assignment (`fn.member = value`): the receiver's type is + * callable and every declaration of the member is such an assignment in a + * JS file. */ +function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + if (access.questionDotToken || !ts.isIdentifier(access.name)) return false; + const recvType = lowerer.typeOf(access.expression); + if (lowerer.checker.getCallSignatures(recvType).length === 0) return false; + const member = lowerer.checker.getPropertyOfType(recvType, access.name.text); + const decls = member ? lowerer.checker.declarationsOf(member) : []; + return ( + decls.length > 0 && + decls.every((d) => { + if (!isJsSourceFile(d.getSourceFile())) return false; + const assign = ts.isBinaryExpression(d) ? d : d.parent; + return ( + assign !== undefined && + ts.isBinaryExpression(assign) && + assign.operatorToken.kind === ts.SyntaxKind.EqualsToken && + ts.isPropertyAccessExpression(assign.left) + ); + }) + ); +} diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index 3baab9dd0c..c114bea4fa 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -18,7 +18,9 @@ import { isRelativeSpecifier } from "../workspace-registry.js"; import { canonicalBuiltinModule, cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, + cjsExportTargetLiteral, entryPackageFilePredicate, isCjsJsFile, isJsSourceFile, @@ -26,6 +28,7 @@ import { locOf, makeCycleAdmission, orderedImportsOf, + requireSpecOf, resolveImport, resolveNpmImport, } from "../program.js"; @@ -36,6 +39,13 @@ import { withCycleInitFlags, } from "./cycle-initialization.js"; import { markRequireEarlyBindings } from "./require-initialization.js"; +import { + cjsTableMethodEntry, + inlineProgramRequireOf, + nestedCjsExportAssignmentsOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; import { invalidJsonModuleDiag, @@ -1391,6 +1401,11 @@ export function collectGlobals( deferJsCollectionDiags(lowerer, sf, diagsBefore); } }; + // Export members assigned in VALUE position at the module's top + // level (`const src = exports.src = []`) are exports too. + for (const nested of nestedCjsExportAssignmentsOf(stmt, cjs?.expr)) { + registerExport(nested.name, nested.name.text, nested.value); + } if (cjs?.kind === "member" && ts.isIdentifier(cjs.name)) { // `exports.C = C` naming a CLASS declaration: alias plumbing — // no storage (a class VALUE global would fence for @@ -1398,6 +1413,13 @@ export function collectGlobals( // symbol (cjsMemberExportClassSymbol) and the class registry // applies unchanged. if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) continue; + // A chain `exports.a = exports.b = value`: every member is an + // export holding the chain's one value. + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null) { + for (const nameNode of chain.names) registerExport(nameNode, nameNode.text, chain.value); + continue; + } registerExport(cjs.name, cjs.name.text, cjs.value); continue; } @@ -1429,7 +1451,22 @@ export function collectGlobals( // statement lowering evaluates the call exactly once at its // source position and assigns that slot. Unmappable calls retain // the existing statement-level fence. - const factoryValued = ts.isCallExpression(rhs); + // Any other computed value (`os.homedir || function () {…}`, + // `cond ? a : b`, `new Registry()`, `ns.member`) is the same single + // snapshot: Node stores whatever the expression produced. A + // 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). + const factoryValued = + !ts.isIdentifier(rhs) && + !ts.isClassExpression(rhs) && + !ts.isObjectLiteralExpression(rhs) && + !( + requireSpecOf(rhs) !== null && + inlineProgramRequireOf(lowerer, rhs) !== null && + !sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "member") + ) && + cjsExportTargetLiteral(rhs, sf) === null; if (cjsScalarLiteral(rhs) || fnValued || factoryValued) { const diagsBefore = lowerer.diags.length; try { @@ -1464,6 +1501,10 @@ export function collectGlobals( if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) { registerExport(prop.name, prop.name.text, prop.initializer); } + // A METHOD entry (`module.exports = { f(x) { … } }`) is an + // expression-valued export whose value is the method's function. + const method = cjsTableMethodEntry(prop); + if (method !== null) registerExport(method.name, method.name.text, method.name); // other property forms fence in the statement lowering } continue; @@ -1527,6 +1568,16 @@ export function collectGlobals( collectNestedVarGlobals(lowerer, sf, stmt, tag); continue; } + // tsc's interop bindings and its then-dead helper declarations own no + // storage either (cjs-tsc-interop.ts). + if ( + isJsSourceFile(sf) && + (isDeadTscHelperDecl(lowerer, stmt) || + (stmt.declarationList.declarations.length === 1 && + tscInteropBindingOfDecl(lowerer, stmt.declarationList.declarations[0]!) !== null)) + ) { + continue; + } // CommonJS require declarations are alias plumbing (preflight owns // the module edges; resolveValueSymbol routes reads through to the // exporter's declarations): no storage, and the statement lowers to @@ -1549,6 +1600,38 @@ export function collectGlobals( stdlibGlobalAliasDecl(lowerer, decl.name, decl.initializer); } } + // `const { codes: { ERR_X } } = require("./errors")`: the outer + // keys stay alias plumbing, but a nested pattern destructures the + // export's value at the require, so its bindings own storage like + // any destructured declaration (lowerVarDeclarator assigns them). + if (ts.isVariableStatement(stmt)) { + const isLetList = + (stmt.declarationList.flags & ts.NodeFlags.Let) !== 0 || + (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) === 0; + for (const decl of stmt.declarationList.declarations) { + for (const element of requireNestedPatternsOf(decl)) { + for (const nameNode of boundIdentifiersOf(element.name!)) { + const symbol = lowerer.checker.getSymbolAtLocation(nameNode); + if (!symbol || lowerer.globalsBySymbol.has(symbol)) continue; + const mapped = lowerer.mapTypeOf(lowerer.typeOf(nameNode)); + const type = mapped && mapped.kind !== "void" ? mapped : DYN; + const g: IrGlobal = { + id: `%g.${tag}${nameNode.text}`, + name: nameNode.text, + type, + mutable: isLetList, + source: bindingSource(nameNode), + ...((type.kind === "func" || type.kind === "dyn") && !isVarDeclared(decl) + ? { tdz: true as const } + : {}), + }; + lowerer.globalsBySymbol.set(symbol, g); + lowerer.globalsList.push(g); + if (isVarDeclared(decl)) noteVarGlobalEntryInit(lowerer, sf, g); + } + } + } + } continue; } // `declare const __VERSION__: string` — AMBIENT: no storage exists diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts index c157496017..292b38e87b 100644 --- a/packages/compiler/src/frontend/lowering/lower-node-module.ts +++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts @@ -8,6 +8,7 @@ import * as win32 from "node:path/win32"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import type { FileParts } from "./lower-modules.js"; +import { lowerCjsExportsObjectValue } from "./cjs-exports-object.js"; import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js"; import { runtimePathForTarget } from "../runtime-resolve.js"; import { @@ -177,6 +178,12 @@ export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null { return id === undefined ? null : numLit(id, locOf(node)); } +/** True when `node` is the CommonJS `require` global (not shadowed by a + * source binding, and not in an ES module). */ +export function isCjsRequireGlobal(lowerer: Lowerer, node: ts.Expression): boolean { + return cjsGlobal(lowerer, node, "require"); +} + export function isCjsModuleGlobal(lowerer: Lowerer, node: ts.Expression): boolean { return cjsGlobal(lowerer, node, "module"); } @@ -428,6 +435,13 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier) if (!isCjsModuleGlobal(lowerer, expr)) return null; const ref = cjsModuleRef(lowerer, expr); if (ref) return ref; + if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) { + lowerer.unsupported( + "SC1090", + expr, + "the CommonJS 'module' object in this position (module.exports members, the whole module.exports in comparisons, and the module metadata fields lower)", + ); + } lowerer.unsupported("SC1090", expr, "the CommonJS 'module' object in an ES module"); } @@ -441,6 +455,10 @@ export function lowerNodeModuleProperty( const member = expr.name.text; if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr)) return boolLit(true, locOf(expr)); + if (member === "exports" && direct) { + const value = lowerCjsExportsObjectValue(lowerer, expr); + if (value) return value; + } if (member === "exports" || member === "require") return null; let moduleRef = direct ? cjsModuleRef(lowerer, expr.expression) diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index f35df085cb..72f49a80bd 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -15,6 +15,19 @@ import { requireInitFlagStmt } from "./require-initialization.js"; import { isJsonRequireDecl } from "./json-require.js"; import { bindingSource } from "../binding-source.js"; import type { Lowerer } from "./lowerer.js"; +import { + cjsExportValueByName, + cjsNamespaceDestructureOf, + cjsTableMethodEntry, + inlineProgramRequireOf, + requireNestedPatternsOf, +} from "./cjs-inline-require.js"; +import { + cjsExportMemberWriteGlobalOf, + isCjsExportsIdentifier, + isFreshExportsReset, +} from "./cjs-exports-object.js"; +import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js"; import { arrayValueRead, arrayValueStore, @@ -60,6 +73,7 @@ import { import { enforceLibBoundary, lowerSurplusCalls } from "./lib-boundary.js"; import { cjsExportAssignmentOf, + cjsExportChainOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, @@ -1553,6 +1567,16 @@ function lowerVarDeclarator( const init = lowerer.requireInitStmt(target.spec, decl); return init ? [init] : []; } + // tsc's interop bindings (`const x_1 = __importDefault(require("./m"))`) + // are require bindings (cjs-tsc-interop.ts): the module's %init here. + if (ts.isSourceFile(stmt.parent)) { + const interop = tscInteropBindingOfDecl(lowerer, decl); + if (interop !== null) { + const init = lowerer.requireInitStmt(interop.spec, decl); + return init ? [init] : []; + } + if (isDeadTscHelperDecl(lowerer, stmt)) return []; + } // CommonJS require declarations (JS files): at the module's top // level the BINDINGS are alias plumbing (no storage; // resolveValueSymbol routes the reads), but the require itself is @@ -1573,7 +1597,21 @@ function lowerVarDeclarator( if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); if (ts.isSourceFile(stmt.parent)) { const init = lowerer.requireInitStmt(spec, decl); - return init ? [init] : []; + const out: IrStmt[] = init ? [init] : []; + // Nested patterns destructure the named export's value right after + // the module evaluated (`const { codes: { ERR_X } } = require(...)`). + for (const element of requireNestedPatternsOf(decl)) { + const key = element.propertyName as ts.Identifier; + const value = lowerer.lowerExpr(key); + lowerer.lowerBindingPattern( + element.name as ts.ObjectBindingPattern, + () => lowerer.lowerExpr(key), + value.type, + isLet, + out, + ); + } + return out; } // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } // = require('child_process'); ... }` — test/common's lazy-getter @@ -1600,6 +1638,35 @@ function lowerVarDeclarator( // `const { createSign } = crypto` over a builtin NAMESPACE binding: // alias plumbing (builtinImportOf routes the reads) — no statement. if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; + // `const { A, B } = ns` over a CommonJS module binding: each name + // snapshots the named export's value here (cjs-inline-require.ts). + { + const nsDestructure = cjsNamespaceDestructureOf(lowerer, decl); + if (nsDestructure !== null) { + const out: IrStmt[] = []; + for (const element of nsDestructure.elements) { + const target = lowerer.globalOf(element.name); + const value = cjsExportValueByName(lowerer, nsDestructure.dep, element.key, element.name); + if (target === null || value === null) { + lowerer.unsupported( + "SC1090", + element.name, + `destructuring the export '${element.key}' of a CommonJS module binding (it has no static value)`, + ); + } + out.push({ + kind: "assign", + localId: target.id, + value: typeEquals(value.type, target.type) + ? value + : lowerer.coerceInto(element.name, value, target.type), + loc: locOf(element.name), + ...("tdz" in target && target.tdz ? { initializes: true as const } : {}), + }); + } + return out; + } + } return lowerer.lowerDestructuringDecl(decl, isLet); } // STORED protocol values with statically closed representations: @@ -6049,6 +6116,49 @@ function lowerCjsExportStatement( if (whole?.kind !== "table" || cjsExportDiscardReason(stmt) !== null) continue; root = lowerer.globalsByDeclNode.get(whole.expr); } + // A CHAIN `exports.a = exports.b = value`: Node evaluates the value + // once, assigns the innermost member first, and every member holds + // the same value. Each member is its own export global + // (collectGlobals registers the inner ones too). + { + const chain = cjsExportChainOf(cjs.expr); + if (chain !== null && root?.type.kind !== "dyn") { + const globalOfName = (nameNode: ts.Identifier): IrGlobal => { + const symbol = + lowerer.checker.getSymbolAtLocation(nameNode) ?? + lowerer.cjsModuleExportSymbol(sf, nameNode.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (!g) { + lowerer.lowerExpr(chain.value); + lowerer.badType(chain.value, lowerer.typeOf(chain.value)); + } + return g; + }; + const inner = chain.names[chain.names.length - 1]!; + const innerG = globalOfName(inner); + const body: IrStmt[] = [ + { + kind: "assign", + localId: innerG.id, + value: lowerer.lowerExprExpecting(chain.value, innerG.type), + loc: locOf(chain.value), + }, + ]; + for (let i = chain.names.length - 2; i >= 0; i--) { + const g = globalOfName(chain.names[i]!); + const read: IrExpr = { kind: "varRef", localId: innerG.id, type: innerG.type, loc }; + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(g.type, innerG.type) + ? read + : lowerer.coerceInto(chain.value, read, g.type), + loc, + }); + } + return { kind: "block", body, loc }; + } + } return assignExport(cjs.name, cjs.value, root); } let tableObj = cjs.obj; @@ -6114,6 +6224,30 @@ function lowerCjsExportStatement( body.push(assignExport(prop.name, prop.initializer)); continue; } + // METHOD entries: the export global holds the method's function, + // created at this statement like a function-expression entry. Node + // calls it with `this` bound to the table, which the static export + // table cannot represent, so a body that uses `this` keeps a fence. + { + const method = cjsTableMethodEntry(prop); + if (method !== null && !resolvedTarget) { + lowerer.rejectThisInObjectMethod(method.body); + const symbol = + lowerer.checker.getSymbolAtLocation(method.name) ?? + lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text); + const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined; + if (g) { + const fn = lowerer.lowerLambda(method); + body.push({ + kind: "assign", + localId: g.id, + value: typeEquals(fn.type, g.type) ? fn : lowerer.coerceInto(method, fn, g.type), + loc: locOf(method), + }); + continue; + } + } + } // ACCESSOR entries: no storage and no code here — reads lift the // getter as a module-level function and call it per read // (cjsExportAccessorRead); writes through the setter keep their @@ -6129,7 +6263,15 @@ function lowerCjsExportStatement( if (ts.isSpreadAssignment(prop)) { let inner: ts.Expression = prop.expression; while (ts.isParenthesizedExpression(inner)) inner = inner.expression; - if (requireSpecOf(inner) !== null) continue; + if (requireSpecOf(inner) !== null) { + // The spread still EVALUATES the module, at its place among the + // table's entries (a program module joins the graph through this + // inline require; cjs-inline-require.ts). + const req = inlineProgramRequireOf(lowerer, inner); + const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null; + if (init !== null && !resolvedTarget) body.push(init); + continue; + } } lowerer.unsupported( "SC1090", @@ -6162,6 +6304,16 @@ function lowerCjsSingleValueExport( ): IrStmt | null { let rhs: ts.Expression = expr.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // `module.exports = require("./x")`: the module re-exports x's exports. + // Requirers resolve through the checker's alias to x's own export + // machinery; this statement is x's evaluation at this position. + { + const req = inlineProgramRequireOf(lowerer, rhs); + // With later member exports (`module.exports.extra = …`) the module + // extends that object, so it keeps the factory root registration. + if (req !== null && !lowerer.globalsByDeclNode.has(expr)) + return lowerer.requireInitStmt(req.spec, req.call) ?? { kind: "block", body: [], loc }; + } if (ts.isIdentifier(rhs)) { const sym = lowerer.resolveValueSymbol(rhs); if (!sym) return null; @@ -6172,6 +6324,17 @@ function lowerCjsSingleValueExport( ) { return { kind: "block", body: [], loc }; } + // A require binding (`const x = require("./x"); module.exports = x`) + // re-exports that module exactly like the inline form (the binding's + // own statement already evaluated it), and a builtin global + // (`module.exports = TypeError`) is the global itself: requirer + // bindings alias straight through to it, so nothing is stored. + if ( + lowerer.checker.declarationsOf(sym).some((d) => ts.isSourceFile(d)) || + lowerer.isStdlibSymbol(sym) + ) { + return { kind: "block", body: [], loc }; + } const g = lowerer.globalsBySymbol.get(sym); if (g) { if (g.mutable) { @@ -6393,6 +6556,21 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm ); } if (topLevelJs && ts.isExpressionStatement(stmtNode)) { + if (isFreshExportsReset(lowerer, stmtNode)) + return { kind: "block", body: [], loc: locOf(expr) }; + // Any other `exports = …` rebinds the wrapper's exports binding: the + // members later attached through it belong to the new value. + if ( + ts.isBinaryExpression(expr) && + expr.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isCjsExportsIdentifier(lowerer, expr.left) + ) { + lowerer.unsupported( + "SC1090", + expr.left, + "rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports. = … and module.exports = value", + ); + } const cjs = cjsExportAssignmentOf(stmtNode); if (cjs) return lowerCjsExportStatement(lowerer, cjs); // The tsc CJS interop stamp — `Object.defineProperty(exports, @@ -6934,6 +7112,19 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm const parts = lowerDestructuringAssignParts(lowerer, expr.left, expr.right, locOf(expr)); return { kind: "block", body: parts.stmts, loc: locOf(expr) }; } + // `exports.x = v` inside a function updates the member's export + // global (cjs-exports-object.ts). + if (ts.isPropertyAccessExpression(expr.left)) { + const g = cjsExportMemberWriteGlobalOf(lowerer, expr.left); + if (g) { + return { + kind: "assign", + localId: g.id, + value: lowerer.lowerExprExpecting(expr.right, g.type), + loc: locOf(expr), + }; + } + } if (!ts.isIdentifier(expr.left)) { lowerer.unsupported("SC1090", expr.left, "assignment to non-variables"); } diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 9df0f01a96..46267b85d2 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -272,6 +272,13 @@ import { appendWorkerModules, } from "./lower-modules.js"; import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js"; +import { + cjsNamespaceModule, + inlineNamespaceRequireOf, + inlineProgramRequireOf, +} from "./cjs-inline-require.js"; +import { tscInteropModuleOf } from "./cjs-tsc-interop.js"; +import { usesCjsExportsIdentity } from "./cjs-exports-object.js"; import { registerBuiltinEventTargetClasses } from "./lower-event-target.js"; import { type ClassInfo, @@ -1863,6 +1870,9 @@ 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(); + /** 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(); /** Bindings whose unchecked string arithmetic can produce either a number * (NaN for an absent read) or a string. Preserve that result union at bare * reads instead of applying the checker-bare string narrowing adapter. */ @@ -3093,8 +3103,10 @@ export class Lowerer { ident.parent.name === ident ) { const recv = ident.parent.expression; - if (ts.isIdentifier(recv) && this.cjsLocalModuleBindingOf(recv)) { - const dep = this.localModuleBindingDepOf(recv); + if (this.cjsLocalModuleBindingOf(recv)) { + const dep = ts.isIdentifier(recv) + ? this.localModuleBindingDepOf(recv) + : this.cjsModuleRefDepOf(recv); const exported = dep ? this.cjsModuleExportSymbol(dep, ident.text) : undefined; // A createRequire call is typed as `any`, so tsgo can manufacture a // transient property symbol with no relation to the target module. @@ -3486,6 +3498,21 @@ export class Lowerer { return dep; } + /** The table module a non-identifier module reference names: an inline + * `require("./x")` (cjs-inline-require.ts) or tsc's unstamped + * `x_1.default` interop read (cjs-tsc-interop.ts). */ + cjsModuleRefDepOf(expr: ts.Expression): ts.SourceFile | null { + const inline = inlineNamespaceRequireOf(this, expr); + if (inline !== null) return inline.dep; + const interop = tscInteropModuleOf(this, expr); + return interop !== null && cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } + + /** The table module a CommonJS module binding identifier names. */ + cjsModuleBindingDepOf(expr: ts.Identifier): ts.SourceFile | null { + return this.localModuleBindingDepOf(expr); + } + /** The compiled module behind a require-like namespace binding: an * ordinary CommonJS require declaration, a canonical createRequire * declaration, or an ESM default import of CommonJS. One resolver keeps @@ -3498,6 +3525,10 @@ export class Lowerer { if (ts.isImportClause(decl)) return this.cjsDefaultImportDepOf(decl); if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name) || !decl.initializer) return null; + { + const interop = tscInteropModuleOf(this, expr); + if (interop !== null) return cjsNamespaceModule(this, interop.dep) ? interop.dep : null; + } const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { if (isJsonRequireDecl(this, decl)) return null; @@ -3544,6 +3575,10 @@ export class Lowerer { if (wholeAssignment === undefined) return null; let rhs: ts.Expression = wholeAssignment.right; while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression; + // A pure re-export (`module.exports = require("./core")`) is the inner + // module's export table, read like any namespace binding. + if (inlineProgramRequireOf(this, rhs) !== null && !this.globalsByDeclNode.has(wholeAssignment)) + return null; if (ts.isIdentifier(rhs)) { const value = this.checker.getSymbolAtLocation(rhs); if (value) { @@ -3596,7 +3631,9 @@ export class Lowerer { * symbols → resolveValueSymbol); the bare value keeps the * namespace-object fence, like ESM namespace imports of builtins. */ cjsLocalModuleBindingOf(expr: ts.Expression): boolean { - if (!ts.isIdentifier(expr)) return false; + // An inline `require("./x")` over a table module reads its members like + // 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); if (this.localModuleBindingDepOf(expr) === null) return false; if (this.createRequireWholeExportSymbolOf(expr) !== null) return false; @@ -3609,6 +3646,8 @@ export class Lowerer { // the object literal) keep the namespace reading — member accesses // resolve through the table's property symbols. if (sym && sym.flags & ts.SymbolFlags.Alias) { + // `module.exports = TypeError`: the binding IS the builtin global. + if (this.isStdlibSymbol(this.checker.getAliasedSymbol(sym))) return false; const d = this.checker.declarationsOf(this.checker.getAliasedSymbol(sym))[0]; if (d && (ts.isClassDeclaration(d) || ts.isFunctionDeclaration(d))) return false; if (d && ts.isVariableDeclaration(d)) { @@ -3706,7 +3745,7 @@ export class Lowerer { const id = `%g.${tag}%loaded`; this.moduleGuardOf.set(fp.sf, id); this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true }); - if (usesCjsTopLevelThis(this, fp.sf)) { + if (usesCjsTopLevelThis(this, fp.sf) || usesCjsExportsIdentity(this, fp.sf)) { const exportsThis: IrGlobal = { id: `%g.${tag}%exportsThis`, name: "%exportsThis", diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index df0cb673e2..158bb2e59b 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1837,6 +1837,36 @@ function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { return out; } +/** INLINE `require("...")` calls in expression position anywhere in a file + * (`require("./x").f`, `require("debug")("ns")`, `module.exports = + * require("./y")`, `{ v: require("./v") }`): each evaluates its module at + * that position, exactly like a bare require statement. Top-level require + * statements and bare require statements are collected by their own + * scans; a require that initializes a nested declaration keeps its + * lowering fence and is not an edge. */ +function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] { + const out: ts.CallExpression[] = []; + ts.walkPreorder(sf, (n) => { + if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return; + let outer: ts.Node = n; + while ( + outer.parent !== undefined && + (ts.isParenthesizedExpression(outer.parent) || + ts.isAsExpression(outer.parent) || + ts.isTypeAssertion(outer.parent) || + ts.isNonNullExpression(outer.parent)) + ) { + outer = outer.parent; + } + const parent = outer.parent; + if (parent !== undefined && ts.isExpressionStatement(parent)) return; + if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer) + return; + out.push(n); + }); + return out; +} + type RequireCallBindingKind7 = "ambient" | "createRequire" | "source"; function stripRequireCasts7(expr: ts.Expression): ts.Expression { @@ -2403,6 +2433,24 @@ function nsBindingUsesAreBareStatements7( export interface CycleEdge { dep: ts.SourceFile; stmt?: ts.ImportDeclaration | ts.ExportDeclaration; + /** An inline require() in a deferred position (a function body): Node + * evaluates the dependency when that code runs, after this module's own + * top level, so the edge orders the dependency after the module and + * never closes a cycle through the module's initialization. */ + lazy?: true; + /** The inline require() call behind the edge (inline edges only). */ + call?: ts.CallExpression; +} + +/** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the static + * export globals do not model; their lowering refuses them by name at run + * time instead of refusing the whole program (the inline call kept its + * runtime fence before it became a module edge). */ +const inlineRequireCycleCalls = new WeakSet(); + +export function isInlineRequireCycleCall(call: ts.Node): boolean { + return inlineRequireCycleCalls.has(call); } /** True when the reference at `node` can only evaluate AFTER the module @@ -4860,6 +4908,21 @@ function preflight7(load: LoadResult): { pattern.elements.every((e) => { if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; if (e.name !== undefined && ts.isIdentifier(e.name)) return true; + // A nested pattern under a named export of a program module + // destructures that export's VALUE at the require + // (`const { codes: { ERR_X } } = require("./errors")`): the + // inner bindings are ordinary destructured storage. + if ( + module === null && + builtin === null && + bindingKind !== "createRequire" && + pattern === req.decl!.name && + e.name !== undefined && + ts.isObjectBindingPattern(e.name) && + e.propertyName !== undefined && + ts.isIdentifier(e.propertyName) + ) + return true; if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) return false; const key = e.propertyName; @@ -4973,6 +5036,32 @@ function preflight7(load: LoadResult): { continue; } } + // Inline requires of program modules are module edges like the bare + // statements above (cjs-inline-require.ts lowers each to the module's + // %init at its position). Only the CommonJS global in a CommonJS file + // counts, and targets outside the compiled graph add nothing here: + // their use sites keep their existing lowering fences. + if (jsRequireFile && !isNodeEsmFile7(sf, program)) { + for (const call of inlineRequiresOf7(sf)) { + if (requireCallBindingKind7(program, call) !== "ambient") continue; + const spec = requireSpecOf7(call)!; + if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue; + const lazy = inDeferredPosition7(call); + const edgeTo = (dep: ts.SourceFile): CycleEdge => + lazy ? { dep, lazy: true, call } : { dep, call }; + const projectDep = resolveImport7(program, sf, spec, "require"); + if (projectDep !== null) { + if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep)); + continue; + } + if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue; + const npmReq = resolveNpmImport7(sf.fileName, spec, "require"); + if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) { + const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile); + if (nDep !== null) deps.push(edgeTo(nDep)); + } + } + } } } @@ -4999,7 +5088,10 @@ function preflight7(load: LoadResult): { // reads are all let/const bindings with module storage — the // lowering checks each one (cycleEarlyBindings) and throws Node's // ReferenceError on a read before the declaration ran. - const cycleAdmissionReason = makeCycleAdmission(program, (sf) => edges.get(sf) ?? []); + const cycleAdmissionReason = makeCycleAdmission(program, (sf) => { + const all = edges.get(sf) ?? []; + return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; + }); const order: ts.SourceFile[] = []; const state = new Map(); const stack: string[] = []; @@ -5008,9 +5100,14 @@ function preflight7(load: LoadResult): { stack.push(sf.fileName); for (const e of edges.get(sf) ?? []) { if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle + if (e.lazy === true) continue; // visited after this module completes const s = state.get(e.dep); if (s === "done") continue; if (s === "visiting") { + if (e.call !== undefined) { + inlineRequireCycleCalls.add(e.call); + continue; + } // A back-edge: Node answers it from the cache (the module is // already evaluating) — admitted by the cheap per-edge rule, the // declaration-only init window rule, or the exact initialization @@ -5034,6 +5131,11 @@ function preflight7(load: LoadResult): { stack.pop(); state.set(sf, "done"); order.push(sf); + // Lazy inline requires run after this module's top level: their + // modules join the order afterwards (cache hits when already loaded). + for (const e of edges.get(sf) ?? []) { + if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep); + } }; visit(entry); @@ -5830,6 +5932,37 @@ export function cjsExportAssignmentOf( return null; } +/** The members and shared value of a CHAINED member export + * `exports.a = exports.b = module.exports.c = value` (outermost name + * first), or null when the assignment is not a chain of at least two + * identifier-named export members or its value is undefined. */ +export function cjsExportChainOf( + e: ts.BinaryExpression, +): { names: ts.Identifier[]; value: ts.Expression } | null { + const names: ts.Identifier[] = []; + let cur: ts.Expression = e; + for (;;) { + while (ts.isParenthesizedExpression(cur)) cur = cur.expression; + if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break; + const left = cur.left; + if ( + !ts.isPropertyAccessExpression(left) || + left.questionDotToken || + !ts.isIdentifier(left.name) + ) + break; + const recv = left.expression; + if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break; + names.push(left.name); + cur = cur.right; + } + // tsc's `exports.a = exports.b = void 0;` preamble is not a value chain + // (the export-statement lowering owns it). + const isVoid = + cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined"); + return names.length >= 2 && !isVoid ? { names, value: cur } : null; +} + /** Why Node would DISCARD this CommonJS export statement, or null when it * survives (Node's export-object identity rules — checker-free, pure * AST): `module.exports = {...}` REPLACES the export object, so members diff --git a/tests/corpus/6150-cjs-inline-require/fallback.js b/tests/corpus/6150-cjs-inline-require/fallback.js new file mode 100644 index 0000000000..d89da1a008 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/fallback.js @@ -0,0 +1,6 @@ +const preferred = undefined; +module.exports = + preferred || + function fallback() { + return "fallback"; + }; diff --git a/tests/corpus/6150-cjs-inline-require/inner.js b/tests/corpus/6150-cjs-inline-require/inner.js new file mode 100644 index 0000000000..7aa065c1e9 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/inner.js @@ -0,0 +1,4 @@ +console.log("inner: evaluated"); +exports.inner = function () { + return "inner"; +}; diff --git a/tests/corpus/6150-cjs-inline-require/late.js b/tests/corpus/6150-cjs-inline-require/late.js new file mode 100644 index 0000000000..a28525b26e --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/late.js @@ -0,0 +1,2 @@ +console.log("late: evaluated"); +exports.value = "late value"; diff --git a/tests/corpus/6150-cjs-inline-require/lib.js b/tests/corpus/6150-cjs-inline-require/lib.js new file mode 100644 index 0000000000..94a5872a43 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/lib.js @@ -0,0 +1,6 @@ +console.log("lib: evaluated"); +exports.f = function (x) { + return x * 10; +}; +exports.n = 7; +exports.counter = { hits: 0 }; diff --git a/tests/corpus/6150-cjs-inline-require/main.js b/tests/corpus/6150-cjs-inline-require/main.js new file mode 100644 index 0000000000..0ebc20d364 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/main.js @@ -0,0 +1,55 @@ +// Inline require() of program modules: each call evaluates its module at +// exactly that position (or answers from the cache), and the value is the +// module's exports, read statically. +console.log("main: start"); +const f = require("./lib.js").f; +console.log("main: first inline require done", f(1)); +console.log("main: cached", require("./lib.js").n, require("./lib.js").f(2)); +exports.viaInline = require("./lib.js").f; +console.log("main: exported member", exports.viaInline(3), exports.viaInline === f); + +// Mutation through an inline read is visible to every requirer. +require("./lib.js").counter.hits++; +const lib = require("./lib.js"); +console.log("main: shared counter", lib.counter.hits, require("./lib.js").counter === lib.counter); + +// A whole-export function called directly off the require. +const debug = require("./make-logger.js")("app"); +console.log(debug("ready")); + +// Construction through a whole-export class and through a member class. +const p = new (require("./point.js"))(1, 2); +console.log("point", p.x + p.y); +const q = new (require("./shapes.js").Square)(3); +console.log("square", q.area()); + +// Lazy inline require: the module loads at the first call, once. +function late() { + return require("./late.js").value; +} +console.log("main: before late"); +console.log("late", late(), late()); + +// Member calls off an inline require of a table module. +console.log("table member", require("./table.js").twice(4), require("./table.js").label); + +// Table entries holding inline requires. +const table = { verify: require("./verify.js"), sign: require("./lib.js").f }; +console.log("table", table.verify("tok"), table.sign(4)); + +// Re-exports: module.exports = require(...) and through a binding. +const a = require("./reexport-inline.js"); +const b = require("./reexport-binding.js"); +console.log("reexports", a.inner(), b.inner(), a.inner === b.inner); + +// Single computed values as the whole export. +const T = require("./type-error.js"); +try { + throw new T("bad input"); +} catch (e) { + console.log(e instanceof TypeError, String(e)); +} +const pick = require("./pick.js"); +console.log("pick", pick(5), typeof require("./fallback.js"), require("./fallback.js")()); +const reg = require("./registry.js"); +console.log("registry", reg.size(), reg.add("x"), reg.size(), require("./registry.js") === reg); diff --git a/tests/corpus/6150-cjs-inline-require/make-logger.js b/tests/corpus/6150-cjs-inline-require/make-logger.js new file mode 100644 index 0000000000..f698b54a72 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/make-logger.js @@ -0,0 +1,6 @@ +console.log("make-logger: evaluated"); +module.exports = function (ns) { + return function (msg) { + return ns + ": " + msg; + }; +}; diff --git a/tests/corpus/6150-cjs-inline-require/package.json b/tests/corpus/6150-cjs-inline-require/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6150-cjs-inline-require/pick.js b/tests/corpus/6150-cjs-inline-require/pick.js new file mode 100644 index 0000000000..aadc1eb8e3 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/pick.js @@ -0,0 +1,7 @@ +function double(x) { + return x * 2; +} +function triple(x) { + return x * 3; +} +module.exports = process.env.SCRIPTC_CORPUS_NEVER_SET ? triple : double; diff --git a/tests/corpus/6150-cjs-inline-require/point.js b/tests/corpus/6150-cjs-inline-require/point.js new file mode 100644 index 0000000000..0f002800c6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/point.js @@ -0,0 +1,6 @@ +module.exports = class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } +}; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-binding.js b/tests/corpus/6150-cjs-inline-require/reexport-binding.js new file mode 100644 index 0000000000..e8307cc659 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-binding.js @@ -0,0 +1,3 @@ +const inner = require("./inner.js"); +console.log("reexport-binding: evaluated"); +module.exports = inner; diff --git a/tests/corpus/6150-cjs-inline-require/reexport-inline.js b/tests/corpus/6150-cjs-inline-require/reexport-inline.js new file mode 100644 index 0000000000..26b6d44ea6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/reexport-inline.js @@ -0,0 +1,2 @@ +console.log("reexport-inline: evaluated"); +module.exports = require("./inner.js"); diff --git a/tests/corpus/6150-cjs-inline-require/registry.js b/tests/corpus/6150-cjs-inline-require/registry.js new file mode 100644 index 0000000000..e34b1015fe --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/registry.js @@ -0,0 +1,13 @@ +class Registry { + constructor() { + this.items = []; + } + add(item) { + this.items.push(item); + return this.items.length; + } + size() { + return this.items.length; + } +} +module.exports = new Registry(); diff --git a/tests/corpus/6150-cjs-inline-require/shapes.js b/tests/corpus/6150-cjs-inline-require/shapes.js new file mode 100644 index 0000000000..4dd4103840 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/shapes.js @@ -0,0 +1,9 @@ +class Square { + constructor(side) { + this.side = side; + } + area() { + return this.side * this.side; + } +} +exports.Square = Square; diff --git a/tests/corpus/6150-cjs-inline-require/table.js b/tests/corpus/6150-cjs-inline-require/table.js new file mode 100644 index 0000000000..ce8f1e364b --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/table.js @@ -0,0 +1,4 @@ +function twice(x) { + return x * 2; +} +module.exports = { twice, label: "table" }; diff --git a/tests/corpus/6150-cjs-inline-require/type-error.js b/tests/corpus/6150-cjs-inline-require/type-error.js new file mode 100644 index 0000000000..483613b44c --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/type-error.js @@ -0,0 +1 @@ +module.exports = TypeError; diff --git a/tests/corpus/6150-cjs-inline-require/verify.js b/tests/corpus/6150-cjs-inline-require/verify.js new file mode 100644 index 0000000000..7adb1f64a6 --- /dev/null +++ b/tests/corpus/6150-cjs-inline-require/verify.js @@ -0,0 +1,4 @@ +console.log("verify: evaluated"); +module.exports = function verify(token) { + return "verified " + token; +}; diff --git a/tests/corpus/6151-cjs-export-chains/chain.js b/tests/corpus/6151-cjs-export-chains/chain.js new file mode 100644 index 0000000000..533e35ba87 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/chain.js @@ -0,0 +1,10 @@ +let evaluations = 0; +function make() { + evaluations++; + return function () { + return "shared"; + }; +} +exports.a = exports.b = make(); +module.exports.c = exports.d = module.exports.e = 5; +exports.evaluations = evaluations; diff --git a/tests/corpus/6151-cjs-export-chains/constants.js b/tests/corpus/6151-cjs-export-chains/constants.js new file mode 100644 index 0000000000..7a67f9f213 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/constants.js @@ -0,0 +1 @@ +exports.VERSION = "1.2.3"; diff --git a/tests/corpus/6151-cjs-export-chains/main.js b/tests/corpus/6151-cjs-export-chains/main.js new file mode 100644 index 0000000000..394c5e3d2e --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/main.js @@ -0,0 +1,9 @@ +// Chained member exports share one value, evaluated once; a fresh +// `exports = module.exports = {}` at the top of a module is a no-op reset. +const chain = require("./chain.js"); +console.log(chain.a(), chain.b(), chain.a === chain.b, chain.evaluations); +console.log(chain.c, chain.d, chain.e); +const re = require("./re.js"); +console.log(re.src.length, re.t.FULL, re.tokens.join(",")); +const sem = require("./sem.js"); +console.log(sem.src.join(" "), sem.re.join(","), sem.count()); diff --git a/tests/corpus/6151-cjs-export-chains/package.json b/tests/corpus/6151-cjs-export-chains/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6151-cjs-export-chains/re.js b/tests/corpus/6151-cjs-export-chains/re.js new file mode 100644 index 0000000000..8839b5cb42 --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/re.js @@ -0,0 +1,15 @@ +'use strict' +const constants = require("./constants.js"); +exports = module.exports = {} +const src = []; +const t = {}; +exports.src = src; +exports.t = t; +exports.tokens = []; +function createToken(name, value) { + t[name] = src.length; + src.push(value); + exports.tokens.push(name); +} +createToken("FULL", "^" + constants.VERSION + "$"); +createToken("LOOSE", "v?" + constants.VERSION); diff --git a/tests/corpus/6151-cjs-export-chains/sem.js b/tests/corpus/6151-cjs-export-chains/sem.js new file mode 100644 index 0000000000..f12b5b25fe --- /dev/null +++ b/tests/corpus/6151-cjs-export-chains/sem.js @@ -0,0 +1,16 @@ +'use strict' +// semver's internal/re.js shape: export members assigned in value position. +exports = module.exports = {} +const re = exports.re = [] +const src = exports.src = [] +const t = exports.t = {} +let R = 0 +const createToken = (name, value) => { + const index = R++ + t[name] = index + src[index] = value + re[index] = value.length +} +exports.count = () => R +createToken('MAIN', 'x.y.z') +createToken('LOOSE', 'v?x') diff --git a/tests/corpus/6152-cjs-tsc-interop/fn.js b/tests/corpus/6152-cjs-tsc-interop/fn.js new file mode 100644 index 0000000000..09b227f5b8 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/fn.js @@ -0,0 +1,3 @@ +module.exports = function square(x) { + return x * x; +}; diff --git a/tests/corpus/6152-cjs-tsc-interop/main.js b/tests/corpus/6152-cjs-tsc-interop/main.js new file mode 100644 index 0000000000..0924dce356 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/main.js @@ -0,0 +1,17 @@ +"use strict"; +// tsc's CommonJS output: interop helpers over require(), the void 0 +// preamble, and the (0, x.default)() call spelling. +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.run = void 0; +console.log("main: start"); +const plain_1 = __importDefault(require("./plain.js")); +const stamped_1 = __importDefault(require("./stamped.js")); +const fn_1 = __importDefault(require("./fn.js")); +function run() { + return [plain_1.default.greet("a"), (0, stamped_1.default)("b"), stamped_1.helper(), (0, fn_1.default)(2)].join(" "); +} +exports.run = run; +console.log(run(), plain_1.default.count, typeof fn_1.default); diff --git a/tests/corpus/6152-cjs-tsc-interop/package.json b/tests/corpus/6152-cjs-tsc-interop/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6152-cjs-tsc-interop/plain.js b/tests/corpus/6152-cjs-tsc-interop/plain.js new file mode 100644 index 0000000000..60e6732ab2 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/plain.js @@ -0,0 +1,5 @@ +console.log("plain: evaluated"); +exports.greet = function (who) { + return "hello " + who; +}; +exports.count = 3; diff --git a/tests/corpus/6152-cjs-tsc-interop/stamped.js b/tests/corpus/6152-cjs-tsc-interop/stamped.js new file mode 100644 index 0000000000..9e844af055 --- /dev/null +++ b/tests/corpus/6152-cjs-tsc-interop/stamped.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.helper = exports.default = void 0; +console.log("stamped: evaluated"); +function greet(who) { + return "stamped " + who; +} +exports.default = greet; +function helper() { + return "helper"; +} +exports.helper = helper; diff --git a/tests/corpus/6153-cjs-exports-object/lib.js b/tests/corpus/6153-cjs-exports-object/lib.js new file mode 100644 index 0000000000..4f69bfca2b --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/lib.js @@ -0,0 +1,13 @@ +exports.lookup = function (ext) { + return "application/" + ext; +}; +exports.contentType = function (ext) { + return exports.lookup(ext) + "; charset=utf-8"; +}; +exports.describe = function () { + return module.exports.lookup("xml"); +}; +// A later reassignment is what member calls see from then on. +exports.lookup = function (ext) { + return "text/" + ext; +}; diff --git a/tests/corpus/6153-cjs-exports-object/main.js b/tests/corpus/6153-cjs-exports-object/main.js new file mode 100644 index 0000000000..39cf61c6ca --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/main.js @@ -0,0 +1,19 @@ +// A module's own exports object, used inside the module: identity +// comparisons (the UMD self-test), typeof and truthiness, and member calls. +const lib = require("./lib.js"); +(function () { + var root = this; + console.log("wrapper", root === module.exports, root === exports, typeof root); +}).call(this); +function check() { + return [exports === module.exports, typeof exports, typeof module.exports, !exports].join(" "); +} +console.log(check(), module.exports ? "truthy" : "falsy"); +console.log(lib.contentType("json"), lib.describe()); +exports.answer = function () { + return 42; +}; +function ask() { + return module.exports.answer() + exports.answer(); +} +console.log(exports.answer(), ask()); diff --git a/tests/corpus/6153-cjs-exports-object/package.json b/tests/corpus/6153-cjs-exports-object/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6153-cjs-exports-object/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6154-cjs-require-nested-destructure/constants.js b/tests/corpus/6154-cjs-require-nested-destructure/constants.js new file mode 100644 index 0000000000..6a53c5d81d --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/constants.js @@ -0,0 +1,8 @@ +exports.MAX_LENGTH = 1024; +exports.SEP = "/"; +exports.helper = function (x) { + return x * 3; +}; +exports.bump = function () { + exports.MAX_LENGTH = 2048; +}; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/errors.js b/tests/corpus/6154-cjs-require-nested-destructure/errors.js new file mode 100644 index 0000000000..bdddfec3fa --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/errors.js @@ -0,0 +1,12 @@ +console.log("errors: evaluated"); +const codes = { + ERR_ONE: { code: "ERR_ONE" }, + ERR_TWO: { code: "ERR_TWO" }, +}; +class AbortError extends Error { + constructor() { + super("aborted"); + this.name = "AbortError"; + } +} +module.exports = { codes, AbortError, version: "4.0" }; diff --git a/tests/corpus/6154-cjs-require-nested-destructure/main.js b/tests/corpus/6154-cjs-require-nested-destructure/main.js new file mode 100644 index 0000000000..d928d5a27b --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/main.js @@ -0,0 +1,14 @@ +// Nested patterns under a named export of a program module destructure +// that export's value at the require (a snapshot), while plain keys stay +// live bindings to the export. +console.log("main: start"); +const { codes: { ERR_ONE, ERR_TWO }, AbortError, version } = require("./errors.js"); +console.log(ERR_ONE.code, ERR_TWO.code, new AbortError().name, version); +const errors = require("./errors.js"); +errors.codes.ERR_ONE = { code: "replaced" }; +console.log(ERR_ONE.code, errors.codes.ERR_ONE.code); +// Destructuring a module BINDING snapshots its exports the same way. +const constants = require("./constants.js"); +const { MAX_LENGTH, SEP: sep, helper } = constants; +constants.bump(); +console.log(MAX_LENGTH, constants.MAX_LENGTH, sep, helper(2)); diff --git a/tests/corpus/6154-cjs-require-nested-destructure/package.json b/tests/corpus/6154-cjs-require-nested-destructure/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6154-cjs-require-nested-destructure/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6155-cjs-export-table-methods/lib.js b/tests/corpus/6155-cjs-export-table-methods/lib.js new file mode 100644 index 0000000000..d8aa56495f --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/lib.js @@ -0,0 +1,20 @@ +console.log("lib: start"); +const WIN = { SEP: "\\" }; +const POSIX = { SEP: "/" }; +function describe() { + return "table"; +} +module.exports = { + scale(x) { + return x * 10; + }, + chars(win32) { + return win32 === true ? WIN.SEP : POSIX.SEP; + }, + get label() { + return describe(); + }, + ...require("./more.js"), + k: (console.log("lib: k entry"), 1), +}; +console.log("lib: end"); diff --git a/tests/corpus/6155-cjs-export-table-methods/main.js b/tests/corpus/6155-cjs-export-table-methods/main.js new file mode 100644 index 0000000000..b2ce150d0c --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/main.js @@ -0,0 +1,5 @@ +// Export tables with method entries, accessors and spreads of required +// modules: the spread evaluates its module at its place in the table. +console.log("main: start"); +const m = require("./lib.js"); +console.log(m.scale(2), m.chars(true), m.chars(false), m.label, m.extra(), m.k); diff --git a/tests/corpus/6155-cjs-export-table-methods/more.js b/tests/corpus/6155-cjs-export-table-methods/more.js new file mode 100644 index 0000000000..4051ce3119 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/more.js @@ -0,0 +1,4 @@ +console.log("more: evaluated"); +exports.extra = function () { + return "more"; +}; diff --git a/tests/corpus/6155-cjs-export-table-methods/package.json b/tests/corpus/6155-cjs-export-table-methods/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6155-cjs-export-table-methods/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs new file mode 100644 index 0000000000..12299e189b --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs @@ -0,0 +1,4 @@ +const status = require("./status.cjs"); +exports.describe = function (code) { + return code + " " + status.message[code]; +}; diff --git a/tests/corpus/6156-cjs-function-members-from-ts/main.ts b/tests/corpus/6156-cjs-function-members-from-ts/main.ts new file mode 100644 index 0000000000..070d576ffa --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/main.ts @@ -0,0 +1,9 @@ +// A CommonJS module exporting a function that carries its own members +// (statuses' `status.message = codes; module.exports = status`), read from +// TypeScript through the default import: calls, member reads, member calls. +import status from "./status.cjs"; +import { describe } from "./consumer.cjs"; + +console.log(status(404), status.message[200], status.codes.length, status.isOk(200)); +console.log(status.redirect[301] === true, status.redirect[302]); +console.log(describe(500)); diff --git a/tests/corpus/6156-cjs-function-members-from-ts/package.json b/tests/corpus/6156-cjs-function-members-from-ts/package.json new file mode 100644 index 0000000000..5ffd9800b9 --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/package.json @@ -0,0 +1 @@ +{ "type": "module" } diff --git a/tests/corpus/6156-cjs-function-members-from-ts/status.cjs b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs new file mode 100644 index 0000000000..7a06d9ab1b --- /dev/null +++ b/tests/corpus/6156-cjs-function-members-from-ts/status.cjs @@ -0,0 +1,10 @@ +function status(code) { + return code + 1; +} +status.message = { 200: "OK", 404: "Not Found", 500: "Internal Server Error" }; +status.codes = [200, 404, 500]; +status.redirect = { 301: true, 302: true }; +status.isOk = function (code) { + return code === 200; +}; +module.exports = status; diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index acaa954791..b9fecac047 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -2214,6 +2214,13 @@ "6143-cjs-require-json/main.js": 3245, "6144-cjs-require-builtin-nested/main.js": 2181, "6145-cjs-require-cycle/main.js": 3507, + "6150-cjs-inline-require/main.js": 4200, + "6151-cjs-export-chains/main.js": 3000, + "6152-cjs-tsc-interop/main.js": 2600, + "6153-cjs-exports-object/main.js": 2400, + "6154-cjs-require-nested-destructure/main.js": 2800, + "6155-cjs-export-table-methods/main.js": 2600, + "6156-cjs-function-members-from-ts/main.ts": 3000, "700-classes-basic.ts": 2530, "701-classes-composition.ts": 2357, "702-classes-this-capture.ts": 2140, @@ -5244,6 +5251,13 @@ "6143-cjs-require-json/main.js": 3050, "6144-cjs-require-builtin-nested/main.js": 3329, "6145-cjs-require-cycle/main.js": 4761, + "6150-cjs-inline-require/main.js": 9600, + "6151-cjs-export-chains/main.js": 7000, + "6152-cjs-tsc-interop/main.js": 6000, + "6153-cjs-exports-object/main.js": 5600, + "6154-cjs-require-nested-destructure/main.js": 6400, + "6155-cjs-export-table-methods/main.js": 6000, + "6156-cjs-function-members-from-ts/main.ts": 7000, "700-classes-basic.ts": 2674, "701-classes-composition.ts": 2606, "702-classes-this-capture.ts": 2403, diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 51f04d3108..564d53bc33 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -972,7 +972,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `(function () { var root = this; console.log("before"); - console.log(root === module.exports); + console.log(root === eval("this")); console.log("after"); }).call(this); `, @@ -983,7 +983,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () "speculative-apply", `(function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); }).apply(null, []); `, "before\n", @@ -994,7 +994,7 @@ describe("refused statements in speculatively lowered JavaScript functions", () `class Base {} class Derived extends (function () { console.log("before"); - console.log(typeof module.exports); + console.log(typeof eval("1")); return Base; })() {} console.log(typeof Derived); @@ -1008,8 +1008,88 @@ console.log(typeof Derived); expect(r.stdout).toBe(stdout); expect(r.stderr).toMatch( new RegExp( - `^Uncaught Error: 'module\\.exports' is part of the standard library types but has no scriptc lowering yet \\[SC2020 at .*${name}\\.cjs:${line}\\]\\n$`, + `^Uncaught Error: values of type '\\(x: string\\) => any' cannot be compiled: .* \\[SC2009 at .*${name}\\.cjs:${line}\\]\\n$`, ), ); }); }); + +// CommonJS forms around the module's exports that keep a refusal name the +// construct itself (they used to report an unlowerable `require` value, a +// "binding form with no lowering", or the ES-module `module` message). +describe("CommonJS export-object and inline require refusals", () => { + test.each([ + [ + "exports-as-argument", + `exports.x = 1; +function keys(o) { + return Object.keys(o); +} +console.log("before"); +console.log(keys(exports)); +`, + {}, + "before\n", + 6, + "'exports' as a value other than a member access, comparison, typeof or truthiness test \\(the compiled exports are a static table, not an object: read exports\\.\\)", + ], + [ + "exports-rebinding", + `console.log("before"); +exports = module.exports = function hash(x) { + return x; +}; +exports.extra = 1; +`, + {}, + "before\n", + 2, + "rebinding 'exports' \\(exports = module\\.exports = value\\)", + ], + [ + "require-computed", + `const name = "./lib.cjs"; +console.log("before"); +console.log(require(name).x); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 3, + "require\\(\\) with a computed specifier", + ], + [ + "exports-call-reads-this", + `exports.timeouts = function (n) { + return this.scale * n; +}; +exports.scale = 2; +console.log("before"); +console.log(exports.timeouts(3)); +`, + {}, + "before\n", + 6, + "calling 'exports\\.timeouts' through the module's exports object when the function may read 'this'", + ], + [ + "require-table-as-value", + `function count(o) { + return Object.keys(o).length; +} +console.log("before"); +console.log(count(require("./lib.cjs"))); +`, + { "lib.cjs": "exports.x = 1;\n" }, + "before\n", + 5, + "the exports object of '\\./lib\\.cjs' as a value", + ], + ] as const)("%s", async (name, source, extra, stdout, line, message) => { + const r = await compileAndRun(name, source, "cjs", extra); + expect(r.exitCode).toBe(1); + expect(r.stdout).toBe(stdout); + expect(r.stderr).toMatch( + new RegExp(`^Uncaught Error: ${message}.*\\[SC1090 at .*${name}\\.cjs:${line}\\]\\n$`), + ); + }); +}); diff --git a/tests/harness/npm.test.ts b/tests/harness/npm.test.ts index c4e70294e7..a025d02747 100644 --- a/tests/harness/npm.test.ts +++ b/tests/harness/npm.test.ts @@ -43,15 +43,15 @@ const KNOWN: Record = { "cjs-lexer-invisible": { build: "SC2013" }, "commander-calc": { run: "SC1090", argvs: [["--help"]] }, "commander-calc-async": { run: "SC1090", argvs: [["--help"]] }, - "create-require": { run: "SC2009" }, - "dotfolder": { run: "SC2009" }, + "create-require": { run: "SC1090" }, + "dotfolder": { run: "SC1090" }, "dual-package-kinds": { build: "SC2013" }, "dynamic-import": { build: "SC2012" }, "dynamic-import-local": { build: "SC2012" }, "esbuild-require": { build: "SC2013" }, "esm-missing-default": { build: "SC1090" }, "esm-missing-export": { build: "SC1090" }, - "fetch-destructure": { build: "SC1090" }, + "fetch-destructure": { run: "SC1031" }, "fs-shims": { run: "SC2020" }, "hoisted-workspace": { build: "SC2013" }, "import-refusal": { build: "SC2020" }, @@ -65,7 +65,7 @@ const KNOWN: Record = { "misc-shims": { build: "SC2013" }, "namespace": { build: "SC1013" }, "node24-async-events": { run: "SC2020" }, - "node24-commonjs-metadata": { run: "SC1090" }, + "node24-commonjs-metadata": { run: "SC2020" }, "node24-crypto-kdf": { run: "SC2020" }, "node24-deep-equality": { run: "SC2020" }, "node24-diagnostics-async": { run: "SC1090" }, From 78100a4124328b7cd11cebda153ff1bb95d18396 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 13:29:28 -0700 Subject: [PATCH 3/4] Record ts7 order baselines for the new CommonJS corpus programs A full SCRIPTC_UPDATE_BASELINES=1 re-record of order-parity.test.ts changed no existing baseline; these are the seven new fixtures (6150-6156). 6150's order places late.js after main.js, the lazy inline-require edge. Co-Authored-By: Claude Opus 5.5 --- .../6150-cjs-inline-require/main.js.json | 20 +++++++++++++++++++ .../6151-cjs-export-chains/main.js.json | 10 ++++++++++ .../corpus/6152-cjs-tsc-interop/main.js.json | 9 +++++++++ .../6153-cjs-exports-object/main.js.json | 7 +++++++ .../main.js.json | 8 ++++++++ .../main.js.json | 8 ++++++++ .../main.ts.json | 8 ++++++++ 7 files changed, 70 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json new file mode 100644 index 0000000000..5f1e322f73 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6150-cjs-inline-require/main.js.json @@ -0,0 +1,20 @@ +{ + "order": [ + "/tests/corpus/6150-cjs-inline-require/lib.js", + "/tests/corpus/6150-cjs-inline-require/inner.js", + "/tests/corpus/6150-cjs-inline-require/reexport-inline.js", + "/tests/corpus/6150-cjs-inline-require/reexport-binding.js", + "/tests/corpus/6150-cjs-inline-require/type-error.js", + "/tests/corpus/6150-cjs-inline-require/pick.js", + "/tests/corpus/6150-cjs-inline-require/registry.js", + "/tests/corpus/6150-cjs-inline-require/make-logger.js", + "/tests/corpus/6150-cjs-inline-require/point.js", + "/tests/corpus/6150-cjs-inline-require/shapes.js", + "/tests/corpus/6150-cjs-inline-require/table.js", + "/tests/corpus/6150-cjs-inline-require/verify.js", + "/tests/corpus/6150-cjs-inline-require/fallback.js", + "/tests/corpus/6150-cjs-inline-require/main.js", + "/tests/corpus/6150-cjs-inline-require/late.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json new file mode 100644 index 0000000000..883aa872a5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6151-cjs-export-chains/main.js.json @@ -0,0 +1,10 @@ +{ + "order": [ + "/tests/corpus/6151-cjs-export-chains/chain.js", + "/tests/corpus/6151-cjs-export-chains/constants.js", + "/tests/corpus/6151-cjs-export-chains/re.js", + "/tests/corpus/6151-cjs-export-chains/sem.js", + "/tests/corpus/6151-cjs-export-chains/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json new file mode 100644 index 0000000000..19a1b20dec --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6152-cjs-tsc-interop/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6152-cjs-tsc-interop/plain.js", + "/tests/corpus/6152-cjs-tsc-interop/stamped.js", + "/tests/corpus/6152-cjs-tsc-interop/fn.js", + "/tests/corpus/6152-cjs-tsc-interop/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json new file mode 100644 index 0000000000..070b131e37 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6153-cjs-exports-object/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6153-cjs-exports-object/lib.js", + "/tests/corpus/6153-cjs-exports-object/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json new file mode 100644 index 0000000000..ee9f71d229 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6154-cjs-require-nested-destructure/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6154-cjs-require-nested-destructure/errors.js", + "/tests/corpus/6154-cjs-require-nested-destructure/constants.js", + "/tests/corpus/6154-cjs-require-nested-destructure/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json new file mode 100644 index 0000000000..9ac5f0e41e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6155-cjs-export-table-methods/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6155-cjs-export-table-methods/more.js", + "/tests/corpus/6155-cjs-export-table-methods/lib.js", + "/tests/corpus/6155-cjs-export-table-methods/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json new file mode 100644 index 0000000000..e73e603e38 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6156-cjs-function-members-from-ts/main.ts.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6156-cjs-function-members-from-ts/status.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/consumer.cjs", + "/tests/corpus/6156-cjs-function-members-from-ts/main.ts" + ], + "diags": [] +} From 238d782fdc94874f0293f3cd20321fbb1112dc61 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 18 Oct 2026 17:17:00 -0500 Subject: [PATCH 4/4] Keep the CommonJS caches on their owners and recount 6110's fences The exports-assignment cache moves from a module-level WeakMap onto the Lowerer (cjsExportsAssignments), and the inline require() calls that close a require cycle move from a module-level WeakSet into preflight's result: checkPreflight stores them on the LoadResult, and lowering receives them with the other preflight facts (LowerOptions.inlineRequireCycleCalls). 6110's call and apply wrappers now compile `root === module.exports` and `typeof module.exports`, which print what Node prints when the wrappers' untaken branches run. The class base's `console.log(module.exports)` is the one statement left as a runtime fence, and its body is lowered once, so the program's deferred-fence count is 1. --- .../frontend/lowering/cjs-exports-object.ts | 9 +++--- .../frontend/lowering/cjs-inline-require.ts | 3 +- .../compiler/src/frontend/lowering/lowerer.ts | 18 ++++++++++++ packages/compiler/src/frontend/pipeline.ts | 1 + packages/compiler/src/frontend/program.ts | 29 ++++++++++--------- .../6110-cjs-speculative-wrapper-fences.cjs | 14 ++++----- 6 files changed, 47 insertions(+), 27 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts index c3edafddcc..d72792ae7c 100644 --- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts +++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts @@ -148,17 +148,16 @@ function truthinessOnly(node: ts.Expression): boolean { } /** Which of the module's exports bindings the file ever assigns (any - * nesting, compound operators included). */ -const assignmentsCache = new WeakMap(); - + * nesting, compound operators included), cached per file on the lowerer + * (Lowerer.cjsExportsAssignments). */ function exportsAssignments( lowerer: Lowerer, sf: ts.SourceFile, ): { exports: boolean; moduleExports: boolean } { - const cached = assignmentsCache.get(sf); + const cached = lowerer.cjsExportsAssignments.get(sf); if (cached !== undefined) return cached; const out = { exports: false, moduleExports: false }; - assignmentsCache.set(sf, out); + lowerer.cjsExportsAssignments.set(sf, out); ts.walkPreorder(sf, (node) => { // A fresh `exports = module.exports = {}` reset leaves both bindings // naming one (new, empty) exports object for the rest of the module. diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts index 75474565b3..7f31337f5b 100644 --- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts +++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts @@ -25,7 +25,6 @@ import { cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, - isInlineRequireCycleCall, isModuleExportsAccess, locOf, npmStaticDepSf7, @@ -109,7 +108,7 @@ 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 (isInlineRequireCycleCall(req.call)) { + if (lowerer.inlineRequireCycleCalls.has(req.call)) { lowerer.unsupported( "SC1090", req.call, diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6da1ede274..55447e1e08 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -753,6 +753,8 @@ export interface LowerOptions { * resolution walk and named-import link checks): the program * compiles to that startup crash. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** LIBRARY mode's reachability roots: the profile-mapped exports of the * entry module. Executable builds root at the entry's top level alone * (an unreferenced export dead-strips); a library's exports are called from @@ -845,6 +847,8 @@ export interface LowererMode { * module init — Node refuses the whole graph before anything evaluates, * so nothing runs. */ startupCrash?: StartupCrash | null; + /** LoadResult.inlineRequireCycleCalls. */ + inlineRequireCycleCalls?: ReadonlySet | undefined; /** The build's outbound native FFI declarations. */ ffiImports?: readonly IrFfiImport[]; /** LowerOptions.libraryCallbacks (see there). */ @@ -958,6 +962,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -981,6 +986,7 @@ export function lowerToIr( projectCheckJs: options.projectCheckJs ?? false, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1011,6 +1017,7 @@ export function lowerToIr( reachable, targetPlatform, startupCrash, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, ffiImports, libraryCallbacks, forkTargets, @@ -1069,6 +1076,7 @@ export function lowerToIr( reachable, remainder: true, alreadyFlushed: resultLowerer.flushedSymbols, + inlineRequireCycleCalls: options.inlineRequireCycleCalls, targetPlatform, ffiImports, libraryCallbacks, @@ -1628,6 +1636,15 @@ export class Lowerer { readonly checker: ts.TypeChecker; readonly diags: ScrDiagnostic[] = []; readonly fnSigsBySymbol = new Map(); + /** Per file: whether it assigns `exports` and `module.exports` + * (cjs-exports-object.ts). */ + readonly cjsExportsAssignments = new Map< + ts.SourceFile, + { exports: boolean; moduleExports: boolean } + >(); + /** Inline require() calls whose module edge closes a require cycle + * (LoadResult.inlineRequireCycleCalls). */ + readonly inlineRequireCycleCalls: ReadonlySet; /** Immutable callable aliases and compiler-generated adapters. The map * is symbol-keyed, so shadowing and cross-module imports keep the same * exact binding discipline as globals/functions. */ @@ -2859,6 +2876,7 @@ export class Lowerer { this.alreadyFlushed = mode.alreadyFlushed ?? new Set(); this.targetPlatform = mode.targetPlatform ?? process.platform; this.startupCrash = mode.startupCrash ?? null; + this.inlineRequireCycleCalls = mode.inlineRequireCycleCalls ?? new Set(); this.projectCheckJs = mode.projectCheckJs ?? false; this.ffiImports = mode.ffiImports ?? []; this.libraryCallbacks = mode.libraryCallbacks ?? false; diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts index 48d7e45a04..f7ab33429e 100644 --- a/packages/compiler/src/frontend/pipeline.ts +++ b/packages/compiler/src/frontend/pipeline.ts @@ -593,6 +593,7 @@ function loadFrontend( ...opts, frontendServices: finalLoad.services, startupCrash: finalLoad.startupCrash ?? null, + inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls, projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false, externalTypes: finalLoad.externalTypes, externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile, diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 47543007f4..8433338d03 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -338,6 +338,12 @@ export interface LoadResult { * last. Empty until checkPreflight runs (and empty when preflight fails * structurally). */ moduleOrder: ts.SourceFile[]; + /** Inline require() calls whose edge closes a require cycle. Node answers + * them with the still-evaluating module's partial exports, which the + * static export globals do not model; their lowering refuses them by name + * at run time instead of refusing the whole program. Filled by + * checkPreflight. */ + inlineRequireCycleCalls?: ReadonlySet; /** Node's startup refusal, when the graph carries an import edge Node * rejects before ANY module evaluates — a RESOLUTION refusal (a bare * specifier nothing installed resolves, an exports/imports map that @@ -975,9 +981,10 @@ export function loadProgram( * preflight diagnostics. Preserve the caller's complete load shape when * mutating it, including its owned services and disposal callback. */ export function checkPreflight(load: T): ScrDiagnostic[] { - const { diags, moduleOrder, startupCrash } = preflight7(load); + const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load); load.moduleOrder = moduleOrder; load.startupCrash = startupCrash; + load.inlineRequireCycleCalls = inlineRequireCycleCalls; return diags; } @@ -2447,17 +2454,6 @@ export interface CycleEdge { call?: ts.CallExpression; } -/** Inline require() calls whose edge closes a require cycle. Node answers - * them with the still-evaluating module's partial exports, which the static - * export globals do not model; their lowering refuses them by name at run - * time instead of refusing the whole program (the inline call kept its - * runtime fence before it became a module edge). */ -const inlineRequireCycleCalls = new WeakSet(); - -export function isInlineRequireCycleCall(call: ts.Node): boolean { - return inlineRequireCycleCalls.has(call); -} - /** True when the reference at `node` can only evaluate AFTER the module * graph finished initializing: inside a function-like BODY, inside a * parameter (default initializers evaluate at call time), or inside an @@ -4133,6 +4129,7 @@ function preflight7(load: LoadResult): { diags: ScrDiagnostic[]; moduleOrder: ts.SourceFile[]; startupCrash: StartupCrash | null; + inlineRequireCycleCalls: ReadonlySet; } { const { program, entry } = load; const diags: ScrDiagnostic[] = [...load.configDiags]; @@ -5118,6 +5115,7 @@ function preflight7(load: LoadResult): { return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all; }); const order: ts.SourceFile[] = []; + const inlineRequireCycleCalls = new Set(); const state = new Map(); const stack: string[] = []; const visit = (sf: ts.SourceFile): void => { @@ -5214,7 +5212,12 @@ function preflight7(load: LoadResult): { esmNamedImportLinkCheck(program, entry, order, diags), ); - return { diags, moduleOrder: order, startupCrash: resolveCrash ?? linkCrash }; + return { + diags, + moduleOrder: order, + startupCrash: resolveCrash ?? linkCrash, + inlineRequireCycleCalls, + }; } /* ── the CJS named-import link check ───────────────────────────────────── diff --git a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs index 1b35465188..fbb1a65fcc 100644 --- a/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs +++ b/tests/corpus/6110-cjs-speculative-wrapper-fences.cjs @@ -1,12 +1,12 @@ -// @deferred-fences: 9 +// @deferred-fences: 1 // Function expressions lowered speculatively — the `.call(this)` and // `.apply` receivers of CommonJS wrappers, and a computed class base — keep -// every statement of their bodies. A statement with no static lowering -// (here `module.exports` read as a value) compiles to its runtime fence -// instead of vanishing; these never run, so the binary matches Node. -// Coverage counts the three fenced statements nine times: each lowering of -// a wrapper body counts its statements, and the `.call` and `.apply` -// wrapper bodies are each lowered four times. +// every statement of their bodies. The wrappers' `root === module.exports` +// and `typeof module.exports` compile; a statement with no static lowering +// (here `module.exports` passed as a value in the class base) compiles to +// its runtime fence instead of vanishing. It never runs, so the binary +// matches Node. Coverage counts that one fenced statement once: the class +// base's body is lowered once. const verbose = process.argv.length > 99; (function () { var root = this;