From 43fb6124388ba2c6d386ec14b6b662ae8acdd210 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 19:58:21 -0700 Subject: [PATCH 1/3] Compile CommonJS requires placed after code that can reach them, and JSON requires A top-level require binding that earlier code could reach was refused (SC1013). The reachability scan now follows Node's evaluation order: hoisted functions and classes run only when reachable code names them, a class statement runs only its heritage, computed keys, decorators and static initializers, timer and promise callbacks run after the module body, and publishing a value through module.exports, exports.n, Object.defineProperty(exports, ...) or F.prototype.m runs nothing until reachable code reads it back. This admits the export-first layouts of semver, glob@7, fs.realpath, on-finished, readable-stream and tsc-emitted CommonJS. A const or let binding that code can still reach before its require now compiles with an initialization flag set right after the require and a module.tdzCheck before the outermost expression of every reference that can run earlier, so an early read throws Node's ReferenceError before any argument evaluates. A reference whose lowering leaves no expression to check, a var binding (Node reads undefined there) and createRequire bindings keep SC1013. require() of a JSON document now answers one parsed object per document, created in the requiring module's %init prelude unless another module already created it, so repeated requires return the same object and mutations are visible to every requirer. Require bindings of JSON documents are ordinary storage, so destructuring (nested patterns, defaults, missing keys) copies at the require as in Node. Nested destructuring of a builtin require binds the members of the sub-module the key names (const { types: { isPromise } } = require('util')). Co-Authored-By: Claude Opus 5.5 --- .../lowering/builtins/module-bindings.ts | 43 +- .../src/frontend/lowering/json-require.ts | 140 ++++ .../src/frontend/lowering/lower-calls.ts | 5 + .../src/frontend/lowering/lower-modules.ts | 10 +- .../src/frontend/lowering/lower-stmts.ts | 157 +++-- .../compiler/src/frontend/lowering/lowerer.ts | 16 + .../lowering/require-initialization.ts | 262 ++++++++ .../compiler/src/frontend/program-analysis.ts | 14 +- packages/compiler/src/frontend/program.ts | 604 ++++++++++++++++-- .../corpus/6140-cjs-require-tdz-throws/lib.js | 8 + .../6140-cjs-require-tdz-throws/main.js | 15 + .../6140-cjs-require-tdz-throws/package.json | 1 + .../6141-cjs-require-tdz-checked/counter.js | 14 + .../6141-cjs-require-tdz-checked/lib.js | 8 + .../6141-cjs-require-tdz-checked/main.js | 49 ++ .../6141-cjs-require-tdz-checked/package.json | 1 + .../comparator.js | 37 ++ .../6142-cjs-require-published-early/debug.js | 10 + .../6142-cjs-require-published-early/glob.js | 20 + .../helpers.js | 10 + .../6142-cjs-require-published-early/main.js | 13 + .../package.json | 1 + .../parse-options.js | 8 + tests/corpus/6143-cjs-require-json/counter.js | 14 + tests/corpus/6143-cjs-require-json/data.json | 7 + tests/corpus/6143-cjs-require-json/list.json | 1 + tests/corpus/6143-cjs-require-json/main.js | 27 + .../corpus/6143-cjs-require-json/package.json | 1 + .../6144-cjs-require-builtin-nested/main.js | 15 + .../package.json | 1 + .../cjs-require-tdz-unchecked/lib.js | 8 + .../cjs-require-tdz-unchecked/main.js | 8 + .../cjs-require-tdz-unchecked/package.json | 3 + tests/diagnostics/cjs-require-tdz/main.js | 13 +- .../cjs-require-tdz-unchecked/main.js.txt | 6 + .../__snapshots__/cjs-require-tdz/main.js.txt | 10 +- 36 files changed, 1418 insertions(+), 142 deletions(-) create mode 100644 packages/compiler/src/frontend/lowering/json-require.ts create mode 100644 packages/compiler/src/frontend/lowering/require-initialization.ts create mode 100644 tests/corpus/6140-cjs-require-tdz-throws/lib.js create mode 100644 tests/corpus/6140-cjs-require-tdz-throws/main.js create mode 100644 tests/corpus/6140-cjs-require-tdz-throws/package.json create mode 100644 tests/corpus/6141-cjs-require-tdz-checked/counter.js create mode 100644 tests/corpus/6141-cjs-require-tdz-checked/lib.js create mode 100644 tests/corpus/6141-cjs-require-tdz-checked/main.js create mode 100644 tests/corpus/6141-cjs-require-tdz-checked/package.json create mode 100644 tests/corpus/6142-cjs-require-published-early/comparator.js create mode 100644 tests/corpus/6142-cjs-require-published-early/debug.js create mode 100644 tests/corpus/6142-cjs-require-published-early/glob.js create mode 100644 tests/corpus/6142-cjs-require-published-early/helpers.js create mode 100644 tests/corpus/6142-cjs-require-published-early/main.js create mode 100644 tests/corpus/6142-cjs-require-published-early/package.json create mode 100644 tests/corpus/6142-cjs-require-published-early/parse-options.js create mode 100644 tests/corpus/6143-cjs-require-json/counter.js create mode 100644 tests/corpus/6143-cjs-require-json/data.json create mode 100644 tests/corpus/6143-cjs-require-json/list.json create mode 100644 tests/corpus/6143-cjs-require-json/main.js create mode 100644 tests/corpus/6143-cjs-require-json/package.json create mode 100644 tests/corpus/6144-cjs-require-builtin-nested/main.js create mode 100644 tests/corpus/6144-cjs-require-builtin-nested/package.json create mode 100644 tests/diagnostics/cjs-require-tdz-unchecked/lib.js create mode 100644 tests/diagnostics/cjs-require-tdz-unchecked/main.js create mode 100644 tests/diagnostics/cjs-require-tdz-unchecked/package.json create mode 100644 tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts index 3fa6eb0c9d..0ccd304b50 100644 --- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts +++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts @@ -34,21 +34,48 @@ export function builtinImportOf( // binding (`const { readFileSync } = require("fs")`, renames via // `{ readFileSync: rf }`) keys the same tables. if (ts.isBindingElement(decl) && decl.name !== undefined && ts.isIdentifier(decl.name)) { - const varDecl = decl.parent?.parent; + // A NESTED pattern names a member of a sub-module (`const { types: { + // isPromise } } = require("util")`): util.types IS the util/types + // module object, so each level composes the module name. + const path: string[] = []; + let pattern: ts.Node | undefined = decl.parent; + let varDecl = pattern?.parent; + while ( + pattern !== undefined && + ts.isObjectBindingPattern(pattern) && + varDecl !== undefined && + ts.isBindingElement(varDecl) + ) { + const key = varDecl.propertyName; + if (varDecl.initializer !== undefined || key === undefined || !ts.isIdentifier(key)) { + return null; + } + path.unshift(key.text); + pattern = varDecl.parent; + varDecl = pattern?.parent; + } if ( - ts.isObjectBindingPattern(decl.parent) && + pattern !== undefined && + ts.isObjectBindingPattern(pattern) && + varDecl !== undefined && ts.isVariableDeclaration(varDecl) && varDecl.initializer !== undefined ) { const spec = requireSpecOf(varDecl.initializer); - const module = + let module = spec !== null ? canonicalBuiltinModule(spec) - : // The one-hop alias twin: `const crypto = require('crypto'); - // const { createSign } = crypto;` — test/common's idiom. The - // destructure re-binds module members under local names, so the - // bindings key the same tables as the direct-require form. - builtinNamespaceDestructureModuleOf(lowerer, varDecl); + : path.length > 0 + ? null + : // The one-hop alias twin: `const crypto = require('crypto'); + // const { createSign } = crypto;` — test/common's idiom. The + // destructure re-binds module members under local names, so the + // bindings key the same tables as the direct-require form. + builtinNamespaceDestructureModuleOf(lowerer, varDecl); + for (const key of path) { + if (module === null) break; + module = canonicalBuiltinModule(`${module}/${key}`); + } if (module === null) return null; const member = decl.propertyName && ts.isIdentifier(decl.propertyName) diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts new file mode 100644 index 0000000000..7c688dcda0 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -0,0 +1,140 @@ +/* CommonJS require() of JSON modules. Node parses the document at the + * first require and caches the resulting object: every later require, from + * any module, answers that same object, so a mutation through one requirer + * is visible to all of them. The document is data known at build time, so + * the object bakes from the parsed text (comptimeValueToIr, as JSON import + * declarations do) into one global per document. The parse has no + * observable effect, so each requiring module creates the object in its + * %init prelude unless an earlier module already did, and every require + * call of the document reads the shared global. A require binding of a + * JSON module is ordinary storage holding that object — destructuring + * copies its properties at the require, exactly like Node. ES-module JSON + * imports keep their own record (Node's ESM loader caches separately). */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { PoisonError } from "./lowerer.js"; +import type { FileParts } from "./lower-modules.js"; +import { isCjsJsFile, locOf, requireSpecOf, resolveImport } from "../program.js"; +import { createRequireCalleeFileOf } from "./builtins/module-bindings.js"; +import { invalidJsonModuleDiag } from "../../diagnostics/diagnostic.js"; +import { BOOL, type IrExpr, type IrGlobal, type IrType } from "../../ir/ir.js"; + +/** One required JSON document: its shared global, the run-once flag, and + * the parsed value with the type it bakes to. */ +export interface JsonRequireModule { + global: IrGlobal; + loadedId: string; + parsed: unknown; + type: IrType; +} + +/** The JSON document a CommonJS `require("…")` call loads, or null. */ +export function jsonRequireTarget( + lowerer: Lowerer, + call: ts.Expression | undefined, +): ts.SourceFile | null { + if (call === undefined || !ts.isCallExpression(call)) return null; + const spec = requireSpecOf(call); + if (spec === null || !ts.isIdentifier(call.expression)) return null; + const sf = call.getSourceFile(); + if (!isCjsJsFile(sf, lowerer.program)) return null; + if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null; + const target = resolveImport(lowerer.program, sf, spec, "require"); + return target !== null && target.fileName.endsWith(".json") ? target : null; +} + +/** A declaration whose initializer is a require() of a JSON document. */ +export function isJsonRequireDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { + return jsonRequireTarget(lowerer, decl.initializer) !== null; +} + +/** Registers the shared global of every JSON document a CommonJS module of + * the program requires, and the guarded creation in each requiring + * module's %init prelude. */ +export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void { + for (const fp of parts) { + const sf = fp.sf; + if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue; + if (!sf.text.includes(".json")) continue; + const calls: ts.CallExpression[] = []; + ts.walkPreorder(sf, (node) => { + if (ts.isCallExpression(node) && requireSpecOf(node) !== null) calls.push(node); + return undefined; + }); + const created = new Set(); + for (const call of calls) { + const jsonSf = jsonRequireTarget(lowerer, call); + if (jsonSf === null) continue; + try { + let mod = lowerer.jsonRequireModules.get(jsonSf.fileName); + if (mod === undefined) { + const tsType = lowerer.typeOf(call); + const mapped = lowerer.mapTypeOf(tsType); + if (!mapped || !lowerer.comptimeBakeable(mapped)) lowerer.badType(call, tsType); + let parsed: unknown; + try { + parsed = JSON.parse(jsonSf.text); + } catch (e) { + lowerer.pushDiag( + invalidJsonModuleDiag( + jsonSf.fileName, + e instanceof Error ? e.message : String(e), + locOf(call), + ), + ); + throw new PoisonError(); + } + const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; + const loadedId = `${id}%loaded`; + lowerer.globalsList.push(global, { + id: loadedId, + name: "%loaded", + type: BOOL, + mutable: true, + }); + mod = { global, loadedId, parsed, type: mapped }; + lowerer.jsonRequireModules.set(jsonSf.fileName, mod); + } + lowerer.jsonRequireCalls.set(call, mod); + if (created.has(mod)) continue; + created.add(mod); + const loc = locOf(call); + const value = lowerer.comptimeValueToIr(mod.parsed, mod.type, "$", call); + const actions = lowerer.jsonInitActions.get(sf) ?? []; + lowerer.jsonInitActions.set(sf, actions); + actions.push({ + kind: "if", + cond: { + kind: "unary", + op: "!", + operand: { kind: "varRef", localId: mod.loadedId, type: BOOL, loc }, + type: BOOL, + loc, + }, + then: [ + { + kind: "assign", + localId: mod.loadedId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + { kind: "assign", localId: mod.global.id, value, loc }, + ], + else_: null, + loc, + }); + } catch (e) { + if (!(e instanceof PoisonError)) throw e; + // diagnostic already recorded + } + } + } +} + +/** A require() call of a JSON document: the shared object. */ +export function lowerJsonRequireCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + const mod = lowerer.jsonRequireCalls.get(call); + if (mod === undefined) return null; + return { kind: "varRef", localId: mod.global.id, type: mod.global.type, loc: locOf(call) }; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2ba83a3d58..4acba2066f 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -58,6 +58,7 @@ import { } from "../../ir/ir.js"; import { lowerProtocolMethodCall } from "./class-protocol-dispatch.js"; import { isJsSourceFile, locOf, requireSpecOf } from "../program.js"; +import { lowerJsonRequireCall } from "./json-require.js"; import { lowerAssertInstanceCall } from "./assert-instances.js"; import { isGenericCallableMemberType, typeKey } from "../type-mapper.js"; import { dynFallbackType, jsFuncNameOf, newFnCtx, staticImportNamespaceType } from "./lowerer.js"; @@ -221,6 +222,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { if (object) return object; } + // require() of a JSON document: the shared parsed object. + const json = lowerJsonRequireCall(lowerer, expr); + if (json) return json; + // 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): diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d18c4f547b..81ded6a585 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -35,6 +35,8 @@ import { withClassInitFlag, withCycleInitFlags, } from "./cycle-initialization.js"; +import { markRequireEarlyBindings } from "./require-initialization.js"; +import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js"; import { invalidJsonModuleDiag, unsupportedPackageImportDiag, @@ -409,6 +411,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts); lowerer.collectNpmImports(parts); lowerer.collectJsonImports(parts); + collectJsonRequires(lowerer, parts); } finally { lowerer.diagSink = null; } @@ -416,6 +419,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts); lowerer.collectNpmImports(parts); lowerer.collectJsonImports(parts); + collectJsonRequires(lowerer, parts); } for (const decl of computed) { const symbol = declSymbolOf(lowerer, decl); @@ -493,6 +497,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void { lowerer.collecting = wasCollecting; } markCycleEarlyBindings(lowerer); + markRequireEarlyBindings(lowerer); } export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void { @@ -1532,8 +1537,9 @@ export function collectGlobals( isRequireStatement(stmt) && !( ts.isVariableStatement(stmt) && - stmt.declarationList.declarations.some((decl) => - isNativeFfiRequire(lowerer, decl.initializer), + stmt.declarationList.declarations.some( + (decl) => + isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl), ) ) ) { diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index 367c147f85..8e43f1a42d 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -10,6 +10,8 @@ import { InternalCompilerError } from "../../errors.js"; * flow (if/while/for/for-of/do, switch, try/catch, abrupt completions), * explicit resource management, and blocked-binding poisoning. */ import * as ts from "../ts7/adapter.js"; +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 { @@ -1512,81 +1514,96 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement): const isLet = (list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0; return list.declarations.flatMap((decl) => { - // The canonical ESM bridge to a compiled program module. Like a - // top-level CommonJS require declaration, the binding is alias - // plumbing while the dependency evaluates at this statement's - // exact position through its run-once initializer. - if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) { - if (!ts.isSourceFile(stmt.parent)) { - lowerer.unsupported( - "SC1090", - decl, - "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)", - ); - } - const target = createRequireProgramModuleOf(lowerer, decl.initializer)!; - const init = lowerer.requireInitStmt(target.spec, decl); - return init ? [init] : []; - } - // 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 - // Node's inline module evaluation — a relative require lowers to the - // required module's guarded %init call at exactly this position - // (first require runs the body, later ones are cache hits; builtins - // load nothing). Binding requires anywhere else would need lazily- - // initialized alias storage this model doesn't represent; named - // fence. - if ( - decl.initializer !== undefined && - requireSpecOf(decl.initializer) !== null && - !isNativeFfiRequire(lowerer, decl.initializer) && - isJsSourceFile(decl.getSourceFile()) - ) { - const spec = requireSpecOf(decl.initializer)!; - if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); - if (ts.isSourceFile(stmt.parent)) { - const init = lowerer.requireInitStmt(spec, decl); - return init ? [init] : []; - } - // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } - // = require('child_process'); ... }` — test/common's lazy-getter - // idiom): builtins load nothing, so the statement is PURE alias - // plumbing wherever it sits — the bindings resolve through the - // same routing top-level requires use, and no statement remains. - // Relative requires keep the fence: their module-init-at-position - // semantics needs storage this model doesn't represent. - if (canonicalBuiltinModule(spec) !== null) { - return []; - } + const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet); + // A require binding earlier code can read is initialized from here on + // (require-initialization.ts). + const initialized = requireInitFlagStmt(lowerer, decl); + return initialized ? [...lowered, initialized] : lowered; + }); +} + +function lowerVarDeclarator( + lowerer: Lowerer, + stmt: ts.VariableStatement, + decl: ts.VariableDeclaration, + isConst: boolean, + isLet: boolean, +): IrStmt[] { + // The canonical ESM bridge to a compiled program module. Like a + // top-level CommonJS require declaration, the binding is alias + // plumbing while the dependency evaluates at this statement's + // exact position through its run-once initializer. + if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) { + if (!ts.isSourceFile(stmt.parent)) { lowerer.unsupported( "SC1090", decl, - "require() with bindings outside the module's top level (move it to the top of the file)", + "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)", ); } - // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over - // the baked constants table: alias plumbing, no storage (uses read - // their literals via builtinConstantBindingOf; collectGlobals skipped - // the globals by the same test). - if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return []; - if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { - // `const { createSign } = crypto` over a builtin NAMESPACE binding: - // alias plumbing (builtinImportOf routes the reads) — no statement. - if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; - return lowerer.lowerDestructuringDecl(decl, isLet); - } - // STORED protocol values with statically closed representations: - // numeric value iterators retain source/cursor/done hidden locals; a - // matchAll drain retains its companion index array so a later - // for-of can serve `m.index`. Both are const-only interceptions. - const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst); - if (numericIterator) return numericIterator; - const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst); - if (drained) return drained; - const lowered = lowerer.lowerVarDecl(decl, isLet); - return lowered ? [lowered] : []; - }); + const target = createRequireProgramModuleOf(lowerer, decl.initializer)!; + const init = lowerer.requireInitStmt(target.spec, decl); + return init ? [init] : []; + } + // 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 + // Node's inline module evaluation — a relative require lowers to the + // required module's guarded %init call at exactly this position + // (first require runs the body, later ones are cache hits; builtins + // load nothing). Binding requires anywhere else would need lazily- + // initialized alias storage this model doesn't represent; named + // fence. + if ( + decl.initializer !== undefined && + requireSpecOf(decl.initializer) !== null && + !isNativeFfiRequire(lowerer, decl.initializer) && + !isJsonRequireDecl(lowerer, decl) && + isJsSourceFile(decl.getSourceFile()) + ) { + const spec = requireSpecOf(decl.initializer)!; + if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false); + if (ts.isSourceFile(stmt.parent)) { + const init = lowerer.requireInitStmt(spec, decl); + return init ? [init] : []; + } + // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync } + // = require('child_process'); ... }` — test/common's lazy-getter + // idiom): builtins load nothing, so the statement is PURE alias + // plumbing wherever it sits — the bindings resolve through the + // same routing top-level requires use, and no statement remains. + // Relative requires keep the fence: their module-init-at-position + // semantics needs storage this model doesn't represent. + if (canonicalBuiltinModule(spec) !== null) { + return []; + } + lowerer.unsupported( + "SC1090", + decl, + "require() with bindings outside the module's top level (move it to the top of the file)", + ); + } + // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over + // the baked constants table: alias plumbing, no storage (uses read + // their literals via builtinConstantBindingOf; collectGlobals skipped + // the globals by the same test). + if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return []; + if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) { + // `const { createSign } = crypto` over a builtin NAMESPACE binding: + // alias plumbing (builtinImportOf routes the reads) — no statement. + if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return []; + return lowerer.lowerDestructuringDecl(decl, isLet); + } + // STORED protocol values with statically closed representations: + // numeric value iterators retain source/cursor/done hidden locals; a + // matchAll drain retains its companion index array so a later + // for-of can serve `m.index`. Both are const-only interceptions. + const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst); + if (numericIterator) return numericIterator; + const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst); + if (drained) return drained; + const lowered = lowerer.lowerVarDecl(decl, isLet); + return lowered ? [lowered] : []; } /** The numeric indexed source of a built-in value-iterator expression: diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 3217802adb..98ae292094 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -10,6 +10,12 @@ import { refuseUncheckedCycleReads, type CycleInitFlag, } from "./cycle-initialization.js"; +import { isJsonRequireDecl, type JsonRequireModule } from "./json-require.js"; +import { + checkRequireBindingAccesses, + type EarlyReference, + type RequireInitFlag, +} from "./require-initialization.js"; import { identityPreservingWidening } from "./coercions/identity.js"; import { scanDivergences } from "./divergences.js"; import { checkedClassAssertion } from "./class-assertions.js"; @@ -2414,6 +2420,10 @@ export class Lowerer { * prepended by lowerFileInit so the bindings are live before any * top-level statement runs. */ readonly jsonInitActions = new Map(); + /** CommonJS require() of JSON documents: the shared object per document + * and the calls reading it (json-require.ts). */ + readonly jsonRequireModules = new Map(); + readonly jsonRequireCalls = new Map(); /** Dynamic `import("literal")` resolutions, keyed * `fileName\u0000specifier` (collectDynamicImports fills it during npm * collection; lowerDynamicImportCall reads it per site). */ @@ -2581,6 +2591,10 @@ export class Lowerer { * by class name (cycle-initialization.ts). */ readonly classInitFlags = new Map(); readonly cycleInitDeclarations = new Map(); + /** CommonJS require declarations whose bindings code running before the + * require can read, and those references (require-initialization.ts). */ + readonly requireInitFlags = new Map(); + readonly requireEarlyReferences: EarlyReference[] = []; get ctx(): FnCtx { const top = this.fnStack[this.fnStack.length - 1]; @@ -3421,6 +3435,7 @@ export class Lowerer { return null; const directSpec = requireSpecOf(decl.initializer); if (directSpec !== null) { + if (isJsonRequireDecl(this, decl)) return null; return ( resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ?? npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require") @@ -6203,6 +6218,7 @@ export class Lowerer { // slots), uniformly across params/locals/globals/fields/body types so // every producer and consumer agrees. Programs with no unregistered // reference are untouched — byte-stability holds. + checkRequireBindingAccesses(this, functions); const module: IrModule | null = this.diags.length > 0 ? null diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts new file mode 100644 index 0000000000..5375f3792e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -0,0 +1,262 @@ +/* CommonJS require bindings read before their require ran. A top-level + * `const x = require("./dep")` binds x AT the require: Node throws "Cannot + * access 'x' before initialization" when code that runs earlier (a hoisted + * function called above the require, say) reads it. The lowering keeps + * require bindings as module aliases, so the reads themselves cannot + * observe the order. When preflight finds code that can reach a binding + * before its require (requireEarlyReference), the declaration gets a BOOL + * initialization flag, set right after the require evaluated the module, + * and every reference that can run earlier checks it first — the same + * module.tdzCheck the import-cycle bindings use. A reference whose lowering + * leaves no expression to check keeps the SC1013 fence. */ +import * as ts from "../ts7/adapter.js"; +import type { Lowerer } from "./lowerer.js"; +import { + inDeferredPosition, + isCjsJsFile, + locOf, + requireEarlyReference, + requireSpecOf, +} from "../program.js"; +import { unsupportedDiag } from "../../diagnostics/diagnostic.js"; +import { + BOOL, + STRING, + VOID, + type IrExpr, + type IrFunction, + type IrStmt, + type SrcLoc, +} from "../../ir/ir.js"; +import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js"; + +/** One flagged require declaration. */ +export interface RequireInitFlag { + flagId: string; + /** The binding names, by symbol. */ + names: Map; +} + +/** A reference that can run before its require: the identifier, the + * outermost expression whose evaluation starts with reading it, and the + * body of the innermost function holding it (null at the top level). */ +export interface EarlyReference { + flag: RequireInitFlag; + name: string; + ref: SrcLoc; + chain: SrcLoc; + body: SrcLoc | null; + node: ts.Identifier; +} + +function boundNames(name: ts.BindingName): ts.Identifier[] { + if (ts.isIdentifier(name)) return [name]; + const out: ts.Identifier[] = []; + for (const el of name.elements) { + if (ts.isOmittedExpression(el) || el.name === undefined) continue; + out.push(...boundNames(el.name)); + } + return out; +} + +/** The outermost node whose evaluation begins by evaluating `ref`: member + * reads, calls and constructions through it, and the transparent wrappers + * around them. */ +function chainOf(ref: ts.Identifier): ts.Node { + let node: ts.Node = ref; + for (;;) { + const parent = node.parent; + if (parent === undefined) return node; + if ( + ((ts.isPropertyAccessExpression(parent) || + ts.isElementAccessExpression(parent) || + ts.isCallExpression(parent) || + ts.isNewExpression(parent)) && + parent.expression === node) || + (ts.isTaggedTemplateExpression(parent) && parent.tag === node) || + ts.isParenthesizedExpression(parent) || + ts.isNonNullExpression(parent) || + (ts.isAsExpression(parent) && parent.expression === node) || + (ts.isSatisfiesExpression(parent) && parent.expression === node) + ) { + node = parent; + continue; + } + return node; + } +} + +function enclosingBody(node: ts.Node): ts.Node | null { + for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if (ts.isFunctionLike(p)) { + const body = (p as { body?: ts.Node }).body; + if (body !== undefined) return body; + } + if (ts.isPropertyDeclaration(p) && p.initializer !== undefined) return p.initializer; + } + return null; +} + +/** Registers a flag for every top-level const/let require declaration of a + * CommonJS module whose bindings earlier code can reach, and collects the + * references that can run before the require. Runs once per lowering pass, + * after global collection. */ +export function markRequireEarlyBindings(lowerer: Lowerer): void { + for (const sf of lowerer.moduleOrder) { + if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue; + const declared: { decl: ts.VariableDeclaration; stmt: ts.VariableStatement }[] = []; + for (const stmt of sf.statements) { + if (!ts.isVariableStatement(stmt)) continue; + if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) continue; + for (const decl of stmt.declarationList.declarations) { + if (decl.initializer === undefined || requireSpecOf(decl.initializer) === null) continue; + if (requireEarlyReference(lowerer.program, decl) === null) continue; + declared.push({ decl, stmt }); + } + } + if (declared.length === 0) continue; + const rawTag = lowerer.fileTag.get(sf) ?? ""; + const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); + const bySymbol = new Map(); + for (const { decl } of declared) { + const flagId = `%g.${tag}%required.${decl.getStart(sf)}`; + lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); + const flag: RequireInitFlag = { flagId, names: new Map() }; + for (const id of boundNames(decl.name)) { + const symbol = lowerer.checker.getSymbolAtLocation(id); + if (symbol === undefined) continue; + flag.names.set(symbol, id.text); + bySymbol.set(symbol, { flag, decl }); + } + lowerer.requireInitFlags.set(decl, flag); + } + const names = new Set(); + for (const { flag } of bySymbol.values()) for (const n of flag.names.values()) names.add(n); + const idents: ts.Identifier[] = []; + ts.walkPreorder(sf, (node) => { + if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node); + return undefined; + }); + lowerer.checker.prefetchSymbolNodesExact(idents); + for (const id of idents) { + const parent = id.parent; + const symbol = + parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id + ? lowerer.checker.getShorthandAssignmentValueSymbol(parent) + : lowerer.checker.getSymbolAtLocation(id); + const owner = symbol === undefined ? undefined : bySymbol.get(symbol); + if (owner === undefined) continue; + if (boundNames(owner.decl.name).includes(id)) continue; + // Code after the require runs after it, except inside functions. + if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue; + const body = enclosingBody(id); + lowerer.requireEarlyReferences.push({ + flag: owner.flag, + name: owner.flag.names.get(symbol!)!, + ref: locOf(id), + chain: locOf(chainOf(id)), + body: body === null ? null : locOf(body), + node: id, + }); + } + } +} + +/** The statement marking a flagged require declaration initialized, placed + * right after its require evaluated the module. */ +export function requireInitFlagStmt(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null { + const flag = lowerer.requireInitFlags.get(decl); + if (flag === undefined) return null; + const loc = locOf(decl); + return { + kind: "assign", + localId: flag.flagId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }; +} + +const within = (inner: SrcLoc, outer: SrcLoc): boolean => + inner.file === outer.file && outer.start <= inner.start && inner.end <= outer.end; + +/** Inserts the initialization check before the outermost expression built + * for each early reference, and refuses a reference whose lowered code + * holds no expression for it. */ +export function checkRequireBindingAccesses(lowerer: Lowerer, functions: IrFunction[]): void { + const refs = lowerer.requireEarlyReferences; + if (refs.length === 0) return; + const byFile = new Map(); + for (const r of refs) { + const list = byFile.get(r.ref.file); + if (list) list.push(r); + else byFile.set(r.ref.file, [r]); + } + const covered = new Set(); + const loweredBodies = new Set(); + const claimed = new Set(); + const check = (r: EarlyReference, loc: SrcLoc): IrStmt => ({ + kind: "exprStmt", + expr: { + kind: "intrinsic", + name: "module.tdzCheck", + args: [ + { kind: "varRef", localId: r.flag.flagId, type: BOOL, loc }, + { kind: "strLit", value: r.name, type: STRING, loc }, + ], + type: VOID, + loc, + }, + loc, + }); + const note = (loc: SrcLoc | undefined): EarlyReference[] => { + if (loc === undefined) return []; + const list = byFile.get(loc.file); + if (list === undefined) return []; + const mine: EarlyReference[] = []; + for (const r of list) { + if (r.body !== null && !loweredBodies.has(r) && within(loc, r.body)) loweredBodies.add(r); + if (!claimed.has(r) && within(r.ref, loc) && within(loc, r.chain)) mine.push(r); + } + return mine; + }; + const rewriteExpr = (e: IrExpr): IrExpr => { + const mine = note(e.loc); + for (const r of mine) claimed.add(r); + const out = mapExprChildren(e, rewriteExpr, rewriteStmt); + for (const r of mine) { + claimed.delete(r); + covered.add(r); + } + if (mine.length === 0) return out; + const flags = new Set(); + const stmts: IrStmt[] = []; + for (const r of mine) { + if (flags.has(r.flag.flagId)) continue; + flags.add(r.flag.flagId); + stmts.push(check(r, e.loc)); + } + return { kind: "seqExpr", stmts, result: out, type: out.type, loc: out.loc }; + }; + const rewriteStmt = (s: IrStmt): IrStmt => { + note(s.loc); + return mapStmtChildren(s, rewriteExpr, rewriteStmt); + }; + for (const fn of functions) fn.body = fn.body.map(rewriteStmt); + if (lowerer.remainder) return; + const reported = new Set(); + for (const r of refs) { + if (covered.has(r)) continue; + // A function body that never lowered never runs. + if (r.body !== null && !loweredBodies.has(r)) continue; + const key = `${r.ref.file}:${r.ref.start}`; + if (reported.has(key)) continue; + reported.add(key); + lowerer.pushDiag( + unsupportedDiag( + "SC1013", + r.ref, + `reading require() binding '${r.name}' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first)`, + ), + ); + } +} diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts index 0ecf09e6a7..e49b9307e4 100644 --- a/packages/compiler/src/frontend/program-analysis.ts +++ b/packages/compiler/src/frontend/program-analysis.ts @@ -1,4 +1,9 @@ -import type { ExportDeclaration, SourceFile, VariableDeclaration } from "./ts7/ast-types.js"; +import type { + ExportDeclaration, + Identifier, + SourceFile, + VariableDeclaration, +} from "./ts7/ast-types.js"; import type { ModuleSourceCandidates } from "./module-source-candidates.js"; import type { CycleEarlyBinding } from "./program.js"; @@ -20,6 +25,11 @@ export class ProgramAnalysis { readonly exactCycleMembers = new Set(); cycleEarlyBindings: EarlyBindingsMemo | null = null; moduleEarlyBindings: EarlyBindingsMemo | null = null; + /** Top-level require declarations: the first reference to their bindings + * that code running before the require can reach (requireEarlyReference). */ + readonly requireEarlyReferences = new Map(); + /** Per file: the first top-level statement that can run user code. */ + readonly requireFirstRunnable = new Map(); clear(): void { this.createRequireReasons.clear(); @@ -29,5 +39,7 @@ export class ProgramAnalysis { this.exactCycleMembers.clear(); this.cycleEarlyBindings = null; this.moduleEarlyBindings = null; + this.requireEarlyReferences.clear(); + this.requireFirstRunnable.clear(); } } diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 3a4ec16479..82b2cb5e0b 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1312,6 +1312,476 @@ export function bindingEarlyUse7( return hit; } +/** The first reference to a top-level require declaration's bindings that + * code running before the require completes can reach, or null when no + * earlier code can run or reach one. Memoized per program: preflight + * decides the fence and the lowering places the initialization checks + * from the same answer. */ +function requireEarlyReference7( + program: ts.Program, + decl: ts.VariableDeclaration, +): ts.Identifier | null { + const memo = program.analysis.requireEarlyReferences; + const cached = memo.get(decl); + if (cached !== undefined) return cached; + let found: ts.Identifier | null = null; + const stmt = decl.parent?.parent; + const sf = decl.getSourceFile(); + if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) { + const stmts = sf.statements; + const k = stmts.indexOf(stmt); + const declarations = stmt.declarationList.declarations; + // Earlier declarators run too: `const x = read(), dep = require('./dep')`. + const precedingDecls = declarations.slice(0, declarations.indexOf(decl)); + let firstRunnable = program.analysis.requireFirstRunnable.get(sf); + if (firstRunnable === undefined) { + firstRunnable = stmts.findIndex((s) => !purePrefixStmt7(program, s)); + program.analysis.requireFirstRunnable.set(sf, firstRunnable); + } + const prefixCanRun = + (firstRunnable >= 0 && firstRunnable < k) || precedingDecls.some((d) => !purePrefixDecl7(d)); + if (prefixCanRun) found = requireBindingEarlyUse7(program, sf, k, decl, precedingDecls); + } + memo.set(decl, found); + return found; +} + +/** The CommonJS twin of bindingEarlyUse7 for a top-level require binding: + * the first reference to one of `decl`'s bindings that code running before + * the require completes can reach, or null. It follows Node's evaluation + * order instead of the conservative "every earlier statement" scan: + * - hoisted functions and classes, and the function and class values a + * var stored before the require, run only when reachable code names + * them; a class statement runs only its heritage, computed keys, + * decorators and static initializers; + * - callbacks handed to timers, microtasks and promise reactions run after + * the module body, hence after the require; + * - publishing a value (`module.exports = f`, `exports.n = f`, + * `Object.defineProperty(exports, "n", { get })`, `F.prototype.m = f`) + * runs nothing. The published value becomes reachable only when + * reachable code reads it back: an `exports.n` read reaches the values + * published as `n`, any other read of the export object reaches all of + * them, and naming `F` reaches what was stored on `F`. No other module + * can read this module's exports before its body completes: require + * cycles keep their SC1016 fence and a compiled module graph has no + * reflective exports object. + * Every other function-like met in reachable code counts as reachable + * because it may be handed to a callee. */ +export function requireBindingEarlyUse7( + program: ts.Program, + sf: ts.SourceFile, + k: number, + decl: ts.VariableDeclaration, + precedingDecls: readonly ts.VariableDeclaration[], +): ts.Identifier | null { + const checker = program.getTypeChecker(); + const bindings = new Set(); + for (const id of boundNames7(decl.name)) { + const sym = checker.getSymbolAtLocation(id); + if (sym) bindings.add(sym); + } + if (bindings.size === 0) return null; + const stmts = sf.statements; + const cjs = isCjsJsFile7(sf, program); + const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => { + const parent = id.parent; + if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) { + return checker.getShorthandAssignmentValueSymbol(parent); + } + return checker.getSymbolAtLocation(id); + }; + // Stored callables and the scans that skip them. + const units = new Map(); + const stored = new Set(); + const addUnit = (name: ts.Identifier, node: ts.Node): void => { + const sym = checker.getSymbolAtLocation(name); + if (sym === undefined) return; + stored.add(node); + const list = units.get(sym); + if (list) list.push(node); + else units.set(sym, [node]); + }; + const storedCallables = (e: ts.Expression, out: ts.Node[]): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) { + out.push(e); + } else if (ts.isObjectLiteralExpression(e)) { + for (const p of e.properties) { + if ( + (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name) + ) { + out.push(p); + } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { + storedCallables(p.initializer, out); + } + } + } + }; + stmts.forEach((s, i) => { + if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) { + addUnit(s.name, s); + } else if (i < k && ts.isVariableStatement(s)) { + for (const d of s.declarationList.declarations) { + if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue; + const out: ts.Node[] = []; + storedCallables(d.initializer, out); + for (const node of out) addUnit(d.name, node); + } + } + }); + // Callbacks that only run as later jobs never run before the require: + // the timers, microtasks and promise reactions. (Event listeners can run + // synchronously from an emit, so they keep the conservative rule.) + const laterJob = (callee: ts.Expression): boolean => { + while (ts.isParenthesizedExpression(callee)) callee = callee.expression; + const name = ts.isIdentifier(callee) + ? callee.text + : ts.isPropertyAccessExpression(callee) + ? callee.name.text + : null; + return ( + name !== null && + (LATER_JOB_BUILTINS7.has(name) || name === "nextTick") && + laterJobCallee7(checker, callee) + ); + }; + if (!hasTopLevelAwait7(sf)) { + const callbacks = (e: ts.Expression): void => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e); + }; + for (const s of stmts.slice(0, k)) { + ts.walkPreorder(s, (node) => { + if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) { + for (const arg of node.arguments ?? []) callbacks(arg); + } + return undefined; + }); + } + } + const isWrapperName = (e: ts.Node, name: string): e is ts.Identifier => + ts.isIdentifier(e) && e.text === name && !sourceBinding7(program, e); + /** `this` at the module's top level: the original export object. */ + const isModuleThis = (e: ts.Node): boolean => { + if (e.kind !== ts.SyntaxKind.ThisKeyword) return false; + for (let p = e.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) return false; + if (ts.isClassExpression(p) || ts.isClassDeclaration(p)) return false; + } + return true; + }; + const isExportObject = (e: ts.Expression): boolean => + isWrapperName(e, "exports") || + isModuleThis(e) || + (isModuleExportsAccess(e) && isWrapperName(e.expression, "module")); + // Published values: by export name ("*" for a whole replacement), and by + // the stored callable whose properties hold them. + const exportLinks = new Map(); + const ownerLinks = new Map(); + const reachedKeys = new Set(); + const reachedUnits = new Set(); + const work: ts.Node[] = []; + let hit: ts.Identifier | null = null; + const reachUnit = (node: ts.Node): void => { + if (reachedUnits.has(node)) return; + reachedUnits.add(node); + work.push(node); + }; + const reachSymbol = (sym: ts.Symbol): void => { + for (const node of units.get(sym) ?? []) reachUnit(node); + for (const node of ownerLinks.get(sym) ?? []) reachUnit(node); + }; + let allReached = false; + const reachKey = (key: string | null): void => { + if (key === null) { + allReached = true; + for (const list of exportLinks.values()) for (const node of list) reachUnit(node); + return; + } + reachedKeys.add(key); + for (const name of [key, "*"]) for (const node of exportLinks.get(name) ?? []) reachUnit(node); + }; + const keyReached = (key: string): boolean => + allReached || reachedKeys.has(key) || (key === "*" && reachedKeys.size > 0); + /** A read of the export object: which published names it can observe. */ + const exportRead = (node: ts.Expression): void => { + let use: ts.Node = node; + while (ts.isParenthesizedExpression(use.parent)) use = use.parent; + const parent = use.parent; + // A truthiness test reads no member (tsc's `this && this.__helper`). + if ( + ts.isBinaryExpression(parent) && + parent.left === use && + parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken + ) { + return; + } + if (ts.isTypeOfExpression(parent)) return; + if (ts.isPropertyAccessExpression(parent) && parent.expression === use) { + reachKey(parent.name.text); + return; + } + if ( + ts.isElementAccessExpression(parent) && + parent.expression === use && + ts.isStringLiteralLike(parent.argumentExpression) + ) { + reachKey(parent.argumentExpression.text); + return; + } + reachKey(null); + }; + const scan = (root: ts.Node): void => { + ts.walkPreorder(root, (n) => { + if (hit !== null) return "stop"; + if (n !== root && stored.has(n)) { + if (ts.isClassExpression(n) || ts.isClassDeclaration(n)) { + for (const part of classSyncParts7(n)) scan(part); + } + return "skip"; + } + // A self-require answers this module's partially built exports. + if (cjs && ts.isCallExpression(n)) { + const spec = requireSpecOf7(n); + if (spec !== null && resolveImport7(program, sf, spec, "require") === sf) reachKey(null); + } + if (cjs && n.kind === ts.SyntaxKind.ThisKeyword && isModuleThis(n)) { + exportRead(n as ts.Expression); + return undefined; + } + if (!ts.isIdentifier(n)) return undefined; + if (inTypePosition7(n)) return undefined; + const named = n.parent; + if ( + (ts.isPropertyAccessExpression(named) || + ts.isPropertyAssignment(named) || + ts.isMethodDeclaration(named) || + ts.isGetAccessorDeclaration(named) || + ts.isSetAccessorDeclaration(named) || + ts.isPropertyDeclaration(named)) && + named.name === n + ) { + return undefined; + } + if (cjs && isWrapperName(n, "exports")) { + exportRead(n); + return undefined; + } + if (cjs && isWrapperName(n, "module")) { + const parent = n.parent; + if (ts.isPropertyAccessExpression(parent) && parent.expression === n) { + if (parent.name.text === "exports") exportRead(parent); + return "skip"; + } + reachKey(null); + return undefined; + } + const sym = symbolOf(n); + if (sym === undefined) return undefined; + if (bindings.has(sym)) { + hit = n; + return "stop"; + } + reachSymbol(sym); + return undefined; + }); + }; + /** Links a published value: evaluating it reads identifiers, but the + * callables it names run only once they are read back. */ + const publish = (value: ts.Expression, link: (node: ts.Node) => void): void => { + while (ts.isParenthesizedExpression(value)) value = value.expression; + if (ts.isIdentifier(value)) { + const sym = symbolOf(value); + if (sym !== undefined && bindings.has(sym)) { + hit = value; + return; + } + if (sym !== undefined) { + // What was stored on the value stays reachable through it. + for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) link(node); + } + return; + } + if (ts.isFunctionExpression(value) || ts.isArrowFunction(value)) { + link(value); + return; + } + if (ts.isClassExpression(value)) { + for (const part of classSyncParts7(value)) scan(part); + link(value); + return; + } + if (ts.isObjectLiteralExpression(value)) { + for (const p of value.properties) { + if (ts.isShorthandPropertyAssignment(p)) { + const sym = checker.getShorthandAssignmentValueSymbol(p); + if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier; + else if (sym !== undefined) { + for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) { + link(node); + } + } + } else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) { + publish(p.initializer, link); + } else if ( + (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name) + ) { + link(p); + } else scan(p); + } + return; + } + if ( + ts.isStringLiteralLike(value) || + ts.isNumericLiteral(value) || + value.kind === ts.SyntaxKind.TrueKeyword || + value.kind === ts.SyntaxKind.FalseKeyword || + value.kind === ts.SyntaxKind.NullKeyword || + (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) + ) { + return; + } + scan(value); + // Whatever an arbitrary expression evaluates to is opaque: treat it as + // reachable through the target too (scan already reached its parts). + }; + /** Where a publication stores: an export name, or a stored callable. */ + type Target = { kind: "key"; key: string } | { kind: "owner"; owner: ts.Symbol }; + const targetOf = (left: ts.Expression): Target | null => { + if (isModuleExportsAccess(left) && isWrapperName(left.expression, "module")) { + return { kind: "key", key: "*" }; + } + const names: string[] = []; + let e: ts.Expression = left; + for (;;) { + if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) { + if (isExportObject(e)) break; + names.unshift(e.name.text); + e = e.expression; + } else if ( + ts.isElementAccessExpression(e) && + !e.questionDotToken && + ts.isStringLiteralLike(e.argumentExpression) + ) { + names.unshift(e.argumentExpression.text); + e = e.expression; + } else break; + } + if (names.length === 0) return null; + if (isExportObject(e)) return { kind: "key", key: names[0]! }; + if (ts.isIdentifier(e)) { + const sym = symbolOf(e); + if (sym !== undefined && units.has(sym) && !bindings.has(sym)) + return { kind: "owner", owner: sym }; + } + return null; + }; + const linkTo = (target: Target, node: ts.Node): void => { + if (target.kind === "key") { + if (keyReached(target.key)) reachUnit(node); + const list = exportLinks.get(target.key); + if (list) list.push(node); + else exportLinks.set(target.key, [node]); + } else { + if ((units.get(target.owner) ?? []).some((u) => reachedUnits.has(u))) reachUnit(node); + if ((units.get(target.owner) ?? []).includes(node)) return; + const list = ownerLinks.get(target.owner); + if (list === undefined) ownerLinks.set(target.owner, [node]); + else if (!list.includes(node)) list.push(node); + } + }; + /** A top-level publication statement, handled in place; false for any + * other statement. */ + const publication = (s: ts.Statement): boolean => { + if (!cjs || !ts.isExpressionStatement(s)) return false; + const e = s.expression; + if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const targets: Target[] = []; + let value: ts.Expression = e; + while ( + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + const target = targetOf(value.left); + if (target === null) return false; + targets.push(target); + value = value.right; + } + publish(value, (node) => { + for (const t of targets) linkTo(t, node); + }); + return true; + } + // Object.defineProperty(exports, "name", { get() {...}, value: f, ... }) + if ( + ts.isCallExpression(e) && + ts.isPropertyAccessExpression(e.expression) && + isWrapperName(e.expression.expression, "Object") && + e.expression.name.text === "defineProperty" && + e.arguments.length === 3 + ) { + const [recv, key, desc] = e.arguments as unknown as [ + ts.Expression, + ts.Expression, + ts.Expression, + ]; + if (!isExportObject(recv) || !ts.isStringLiteralLike(key)) return false; + if (!ts.isObjectLiteralExpression(desc)) return false; + const keyTarget: Target = { kind: "key", key: key.text }; + const link = (node: ts.Node): void => linkTo(keyTarget, node); + for (const p of desc.properties) { + if ( + (ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) && + ts.isIdentifier(p.name) && + (p.name.text === "get" || p.name.text === "set") + ) { + if (ts.isMethodDeclaration(p)) link(p); + else publish(p.initializer, link); + } else if ( + ts.isPropertyAssignment(p) && + ts.isIdentifier(p.name) && + p.name.text === "value" + ) { + publish(p.initializer, link); + } else scan(p); + } + return true; + } + return false; + }; + const drain = (): void => { + while (hit === null && work.length > 0) { + const unit = work.pop()!; + checker.prefetchSymbolRoots([unit]); + scan(unit); + } + }; + const roots = stmts.slice(0, k); + checker.prefetchSymbolRoots([...roots, ...precedingDecls]); + for (const s of roots) { + if (hit !== null) break; + if (ts.isFunctionDeclaration(s)) continue; + if (ts.isClassDeclaration(s)) { + for (const part of classSyncParts7(s)) scan(part); + } else if (!publication(s)) { + scan(s); + } + drain(); + } + for (const d of precedingDecls) { + if (hit !== null) break; + scan(d); + drain(); + } + return hit; +} + /** Bare `require("...")` EXPRESSION STATEMENTS anywhere below the top * level of a file — inside top-level ifs/blocks/loops and inside function * bodies (the lazy-require idiom). No bindings, pure side-effect loads. */ @@ -4049,10 +4519,37 @@ function preflight7(load: LoadResult): { { const jsRequireFile = isJsSourceFileName(sf.fileName); const stmts = sf.statements; - let firstRunnable = -1; - stmts.forEach((s, i) => { - if (firstRunnable < 0 && !purePrefixStmt7(program, s)) firstRunnable = i; - }); + // A binding that code running before the require can reach. Node + // initializes it AT the require: a const/let read earlier throws + // ReferenceError, which the lowering reproduces with an + // initialization check at every reference that can run early + // (require-initialization.ts). A var reads undefined there, which + // module aliases cannot represent; createRequire bindings have no + // checked form either. + const earlyRequireFence = ( + req: { decl: ts.VariableDeclaration | null }, + bindingKind: RequireCallBindingKind7, + loc: { file: string; start: number; end: number }, + ): boolean => { + const early = req.decl !== null ? requireEarlyReference7(program, req.decl) : null; + if ( + early === null || + (bindingKind !== "createRequire" && + ((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0) + ) { + return false; + } + diags.push( + unsupportedDiag( + "SC1013", + loc, + bindingKind === "createRequire" + ? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)` + : `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`, + ), + ); + return true; + }; for (let k = 0; k < stmts.length; k++) { const stmt = stmts[k]!; for (const req of requiresOf7(stmt)) { @@ -4072,24 +4569,62 @@ function preflight7(load: LoadResult): { if (load.externalTypes.has(req.spec)) { continue; } + // A JSON document: the require answers its cached parsed object + // (json-require.ts), an ordinary value the binding stores, so + // every destructuring form lowers as usual. No module edge. + const jsonDep = resolveImport7(program, sf, req.spec, "require"); + if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) { + if (bindingKind === "createRequire") continue; + if (ts.isVariableStatement(stmt) && requiresOf7(stmt).length > 1) { + const others = requiresOf7(stmt).some((r) => { + const d = resolveImport7(program, sf, r.spec, "require"); + return d === null || !d.fileName.endsWith(".json"); + }); + if (others) { + diags.push( + unsupportedDiag( + "SC1012", + loc, + "require() of a JSON module declared together with other require() bindings", + ), + ); + continue; + } + } + earlyRequireFence(req, bindingKind, loc); + continue; + } if (req.decl && ts.isArrayBindingPattern(req.decl.name)) { diags.push(unsupportedDiag("SC1012", loc, "array-destructuring require() bindings")); continue; } if (req.decl && ts.isObjectBindingPattern(req.decl.name)) { - const bad = req.decl.name.elements.find( - (e) => - e.name === undefined || - !ts.isIdentifier(e.name) || - e.initializer !== undefined || - e.dotDotDotToken !== undefined, - ); - if (bad) { + // A nested pattern of a builtin binds members of the sub-module + // its key names (`const { types: { isPromise } } = + // require("util")` — util.types IS util/types). + const builtin = canonicalBuiltinModule(req.spec); + const plainElements = ( + pattern: ts.ObjectBindingPattern, + module: string | null, + ): boolean => + pattern.elements.every((e) => { + if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false; + if (e.name !== undefined && ts.isIdentifier(e.name)) return true; + if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name)) + return false; + const key = e.propertyName; + if (key === undefined || !ts.isIdentifier(key)) return false; + const sub = canonicalBuiltinModule(`${module}/${key.text}`); + return sub !== null && plainElements(e.name, sub); + }); + if (!plainElements(req.decl.name, builtin)) { diags.push( unsupportedDiag( "SC1012", loc, - "require() destructuring with defaults, rest, or nested patterns", + builtin !== null + ? "require() destructuring with defaults, rest, or nested patterns other than builtin sub-modules" + : "require() destructuring with defaults, rest, or nested patterns of program modules (JSON documents and builtin sub-modules destructure fully)", ), ); continue; @@ -4132,38 +4667,7 @@ function preflight7(load: LoadResult): { continue; } } - if (dep && dep.fileName.endsWith(".json")) { - if (bindingKind === "createRequire") continue; - diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); - continue; - } - // Earlier declarators run too: `const x = read(), dep = - // require('./dep')` must not lose the guard merely because both - // initializers share one VariableStatement. - const precedingDecls = - req.decl && ts.isVariableStatement(stmt) - ? stmt.declarationList.declarations.slice( - 0, - stmt.declarationList.declarations.indexOf(req.decl), - ) - : []; - const prefixCanRun = - (firstRunnable >= 0 && firstRunnable < k) || - precedingDecls.some((decl) => !purePrefixDecl7(decl)); - const tdzName = - prefixCanRun && req.decl - ? bindingEarlyUse7(program, sf, k, req.decl, precedingDecls) - : null; - if (tdzName !== null) { - diags.push( - unsupportedDiag( - "SC1013", - loc, - `require() below code that can reach its binding '${tdzName}' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)`, - ), - ); - continue; - } + if (earlyRequireFence(req, bindingKind, loc)) continue; if (dep) deps.push({ dep }); } } @@ -4191,11 +4695,9 @@ function preflight7(load: LoadResult): { } const projectDep = resolveImport7(program, sf, spec, "require"); if (projectDep !== null) { - if (projectDep.fileName.endsWith(".json")) { - if (bindingKind === "createRequire") continue; - diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules")); - continue; - } + // A bare require of a JSON document only parses it: nothing to + // evaluate (json-require.ts creates the cached object). + if (projectDep.fileName.endsWith(".json")) continue; deps.push({ dep: projectDep }); continue; } @@ -4930,6 +5432,8 @@ export { resolveImport7 as resolveImport, resolveNpmImport7 as resolveNpmImport, locOf7 as locOf, + requireEarlyReference7 as requireEarlyReference, + inDeferredPosition7 as inDeferredPosition, }; /** True for JavaScript source files (.js/.mjs/.cjs) — the files whose diff --git a/tests/corpus/6140-cjs-require-tdz-throws/lib.js b/tests/corpus/6140-cjs-require-tdz-throws/lib.js new file mode 100644 index 0000000000..2a9669e123 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/corpus/6140-cjs-require-tdz-throws/main.js b/tests/corpus/6140-cjs-require-tdz-throws/main.js new file mode 100644 index 0000000000..13163137e2 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/main.js @@ -0,0 +1,15 @@ +// @exit: 1 +// A hoisted function called above the require reads the binding before +// the require ran: Node throws "Cannot access 'greet' before +// initialization" there, uncaught, and nothing after it runs. +'use strict'; + +console.log('start'); +console.log('boot', helper()); +const { greet } = require('./lib.js'); + +function helper() { + return greet('early'); +} + +console.log('unreachable', helper()); diff --git a/tests/corpus/6140-cjs-require-tdz-throws/package.json b/tests/corpus/6140-cjs-require-tdz-throws/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6140-cjs-require-tdz-throws/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6141-cjs-require-tdz-checked/counter.js b/tests/corpus/6141-cjs-require-tdz-checked/counter.js new file mode 100644 index 0000000000..24ef13e5de --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/counter.js @@ -0,0 +1,14 @@ +'use strict'; + +class Counter { + /** @param {number} start */ + constructor(start) { + this.value = start; + } + next() { + this.value += 1; + return this.value; + } +} + +module.exports = Counter; diff --git a/tests/corpus/6141-cjs-require-tdz-checked/lib.js b/tests/corpus/6141-cjs-require-tdz-checked/lib.js new file mode 100644 index 0000000000..aa15c97d6a --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet, answer: 42 }; diff --git a/tests/corpus/6141-cjs-require-tdz-checked/main.js b/tests/corpus/6141-cjs-require-tdz-checked/main.js new file mode 100644 index 0000000000..917eb78743 --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/main.js @@ -0,0 +1,49 @@ +// Require bindings that code running before the require can read. Every +// read that happens before the require throws Node's ReferenceError for +// the local binding name, before any argument of the access evaluates; +// the same reads succeed once the require ran. +'use strict'; + +/** + * @param {string} label + * @param {() => unknown} read + */ +function attempt(label, read) { + try { + console.log(label, 'ok', read()); + } catch (e) { + if (!(e instanceof ReferenceError)) throw e; + console.log(label, e.name, e.message); + } +} + +/** @param {string} s */ +function note(s) { + console.log('evaluated', s); + return s; +} + +const later = [() => greet('later'), () => new Counter(5).next()]; + +attempt('call', () => greet('a')); +attempt('call-arguments', () => greet(note('argument'))); +attempt('member', () => lib.greet('b')); +attempt('construct', () => new Counter(1).next()); +attempt('builtin', () => path.join('a', 'b')); +attempt('value', () => answer + 1); +attempt('renamed', () => hello('c')); + +const { greet } = require('./lib.js'); +const lib = require('./lib.js'); +const Counter = require('./counter.js'); +const path = require('path'); +const { answer, greet: hello } = require('./lib.js'); + +attempt('call', () => greet('a')); +attempt('call-arguments', () => greet(note('argument'))); +attempt('member', () => lib.greet('b')); +attempt('construct', () => new Counter(1).next()); +attempt('builtin', () => path.join('a', 'b')); +attempt('value', () => answer + 1); +attempt('renamed', () => hello('c')); +for (const read of later) console.log('later', read()); diff --git a/tests/corpus/6141-cjs-require-tdz-checked/package.json b/tests/corpus/6141-cjs-require-tdz-checked/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6141-cjs-require-tdz-checked/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6142-cjs-require-published-early/comparator.js b/tests/corpus/6142-cjs-require-published-early/comparator.js new file mode 100644 index 0000000000..2b2ea6146d --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/comparator.js @@ -0,0 +1,37 @@ +'use strict'; + +// The semver layout: the class and its export come first, the requires its +// methods use come after. Publishing the class runs none of its methods, so +// no read can happen before the requires ran. +const ANY = Symbol('ANY'); + +class Comparator { + static get ANY() { + return ANY; + } + + static describe() { + return `Comparator(${typeof parseOptions})`; + } + + /** + * @param {string} text + * @param {{ loose?: boolean }} [options] + */ + constructor(text, options) { + this.mode = parseOptions(options); + this.text = text.trim(); + debug('comparator', this.text, this.mode); + } + + /** @param {string} other */ + test(other) { + return path.basename(other) === this.text; + } +} + +module.exports = Comparator; + +const parseOptions = require('./parse-options.js'); +const { debug } = require('./debug.js'); +const path = require('path'); diff --git a/tests/corpus/6142-cjs-require-published-early/debug.js b/tests/corpus/6142-cjs-require-published-early/debug.js new file mode 100644 index 0000000000..30451569b1 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/debug.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('debug: init'); + +/** @param {...unknown} args */ +function debug(...args) { + console.log('debug:', args.join(' ')); +} + +module.exports = { debug }; diff --git a/tests/corpus/6142-cjs-require-published-early/glob.js b/tests/corpus/6142-cjs-require-published-early/glob.js new file mode 100644 index 0000000000..3d2907f005 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/glob.js @@ -0,0 +1,20 @@ +'use strict'; + +// The glob@7 layout with var: export first, then var requires, a runnable +// statement between them, and properties stored on the exported function. +module.exports = glob; +glob.sync = globSync; + +var parseOptions = require('./parse-options.js'); +var mode = parseOptions({ loose: true }); +var helpers = require('./helpers.js'); + +/** @param {string} pattern */ +function glob(pattern) { + return `${mode}:${helpers.normalize(pattern)}`; +} + +/** @param {string} pattern */ +function globSync(pattern) { + return helpers.normalize(pattern).toUpperCase(); +} diff --git a/tests/corpus/6142-cjs-require-published-early/helpers.js b/tests/corpus/6142-cjs-require-published-early/helpers.js new file mode 100644 index 0000000000..206ca77851 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/helpers.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('helpers: init'); + +/** @param {string} s */ +function normalize(s) { + return s.replace(/\/+/g, '/'); +} + +module.exports = { normalize }; diff --git a/tests/corpus/6142-cjs-require-published-early/main.js b/tests/corpus/6142-cjs-require-published-early/main.js new file mode 100644 index 0000000000..9182f1cf5b --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/main.js @@ -0,0 +1,13 @@ +// CommonJS modules that export first and require afterwards: code that can +// reach the bindings only runs after the module finished, so they compile +// without a fence and behave exactly as under Node. +'use strict'; + +console.log('main: start'); +const Comparator = require('./comparator.js'); +const glob = require('./glob.js'); + +const c = new Comparator(' a.txt ', { loose: true }); +console.log(c.mode, c.text, c.test('/x/a.txt'), Comparator.ANY.toString()); +console.log(new Comparator('b').mode, Comparator.describe()); +console.log(glob('a//b'), glob.sync('c//d')); diff --git a/tests/corpus/6142-cjs-require-published-early/package.json b/tests/corpus/6142-cjs-require-published-early/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6142-cjs-require-published-early/parse-options.js b/tests/corpus/6142-cjs-require-published-early/parse-options.js new file mode 100644 index 0000000000..0872049ea3 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/parse-options.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {{ loose?: boolean } | undefined} options */ +function parseOptions(options) { + return options && options.loose ? 'loose' : 'strict'; +} + +module.exports = parseOptions; diff --git a/tests/corpus/6143-cjs-require-json/counter.js b/tests/corpus/6143-cjs-require-json/counter.js new file mode 100644 index 0000000000..b794d1d4c8 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/counter.js @@ -0,0 +1,14 @@ +'use strict'; + +// A second requirer: it receives the same cached object and mutates it. +const data = require('./data.json'); + +console.log('counter: sees', data.count); +data.count += 1; + +function bump() { + data.count += 100; + data.tags.push('bumped'); +} + +module.exports = { bump }; diff --git a/tests/corpus/6143-cjs-require-json/data.json b/tests/corpus/6143-cjs-require-json/data.json new file mode 100644 index 0000000000..b1cf3d3bc9 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/data.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-data", + "version": "1.2.3", + "count": 41, + "tags": ["native", "json"], + "nested": { "flag": true, "ratio": 0.5 } +} diff --git a/tests/corpus/6143-cjs-require-json/list.json b/tests/corpus/6143-cjs-require-json/list.json new file mode 100644 index 0000000000..98ace19c09 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/list.json @@ -0,0 +1 @@ +[3, 1, 2] diff --git a/tests/corpus/6143-cjs-require-json/main.js b/tests/corpus/6143-cjs-require-json/main.js new file mode 100644 index 0000000000..b67c39f3eb --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/main.js @@ -0,0 +1,27 @@ +// require() of a JSON document: Node parses it at the first require and +// caches the object, so every require of the document, from any module, +// returns the same object and a mutation is visible to all requirers. +// Destructuring copies the properties at the require. +'use strict'; + +const data = require('./data.json'); +const { count, nested: { flag, ratio }, tags: [firstTag] } = require('./data.json'); +const version = require('./data.json').version; + +console.log(data.name, version, data.count, count, flag, ratio, firstTag); +console.log('same object', require('./data.json') === data); + +const { bump } = require('./counter.js'); +console.log('after counter', data.count, count); +bump(); +console.log('after bump', data.count, data.tags.join(','), count); + +function load() { + return require('./data.json'); +} +console.log('lazy same object', load() === data, load().count); +load().nested.flag = false; +console.log('flag', data.nested.flag, flag); + +const list = require('./list.json'); +console.log(list.length, list.slice().sort().join(','), require('./list.json') === list); diff --git a/tests/corpus/6143-cjs-require-json/package.json b/tests/corpus/6143-cjs-require-json/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6143-cjs-require-json/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6144-cjs-require-builtin-nested/main.js b/tests/corpus/6144-cjs-require-builtin-nested/main.js new file mode 100644 index 0000000000..3058767623 --- /dev/null +++ b/tests/corpus/6144-cjs-require-builtin-nested/main.js @@ -0,0 +1,15 @@ +// Nested destructuring of a builtin require (undici's `const { types: { +// isPromise } } = require('util')`): util.types IS the util/types module, +// so the nested pattern binds that module's members. +'use strict'; + +const { + types: { isPromise, isPromise: promiseCheck }, + format, +} = require('util'); +const { promises: { readFile } } = require('fs'); +const { posix: { join } } = require('path'); + +console.log(isPromise(Promise.resolve(1)), isPromise({}), promiseCheck(Promise.reject(new Error('x')).catch(() => {}))); +console.log(format('%s=%d', 'a', 1), join('a', 'b', '..', 'c')); +readFile(__filename, 'utf8').then((text) => console.log('read', text.includes('isPromise'))); diff --git a/tests/corpus/6144-cjs-require-builtin-nested/package.json b/tests/corpus/6144-cjs-require-builtin-nested/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6144-cjs-require-builtin-nested/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/lib.js b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js new file mode 100644 index 0000000000..2a9669e123 --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js @@ -0,0 +1,8 @@ +'use strict'; + +/** @param {string} who */ +function greet(who) { + return `hello ${who}`; +} + +module.exports = { greet }; diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/main.js b/tests/diagnostics/cjs-require-tdz-unchecked/main.js new file mode 100644 index 0000000000..52b12fcb47 --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/main.js @@ -0,0 +1,8 @@ +// A const require binding read by a statement above the require that +// lowers to no expression: publishing the binding as an export is module +// plumbing, so no initialization check can run where Node throws +// "Cannot access 'greet' before initialization". The read keeps SC1013. +'use strict'; + +module.exports = { greet }; +const { greet } = require('./lib.js'); diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/package.json b/tests/diagnostics/cjs-require-tdz-unchecked/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/tests/diagnostics/cjs-require-tdz-unchecked/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/diagnostics/cjs-require-tdz/main.js b/tests/diagnostics/cjs-require-tdz/main.js index a571b45242..c2b5a945ce 100644 --- a/tests/diagnostics/cjs-require-tdz/main.js +++ b/tests/diagnostics/cjs-require-tdz/main.js @@ -1,14 +1,15 @@ -// A require whose binding IS reachable from code above it: Node would -// throw "Cannot access 'greet' before initialization" at the helper() -// call (the const initializes AT the require), an ordering the compiled -// aliasing does not model — the require keeps a named SC1013 fence. +// A var require binding that code above the require can reach: Node reads +// undefined there (the var is hoisted, the require has not run), a value +// the compiled module alias cannot hold — the require keeps a named SC1013 +// fence. A const or let binding compiles with an initialization check +// instead (tests/corpus/6141-cjs-require-tdz-checked). 'use strict'; console.log('boot', helper()); -const { greet } = require('./lib.js'); +var lib = require('./lib.js'); function helper() { - return greet('early'); + return lib.greet('early'); } console.log(helper()); diff --git a/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt new file mode 100644 index 0000000000..eda869e9a6 --- /dev/null +++ b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt @@ -0,0 +1,6 @@ +cjs-require-tdz-unchecked/main.js:7:20 - error SC1013: reading require() binding 'greet' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first) is not supported yet + + 6 | + 7 | module.exports = { greet }; + | ^~~~~ + 8 | const { greet } = require('./lib.js'); \ No newline at end of file diff --git a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt index 6316ce98b4..295f7cd0c1 100644 --- a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt +++ b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt @@ -1,6 +1,6 @@ -cjs-require-tdz/main.js:8:7 - error SC1013: require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet +cjs-require-tdz/main.js:9:5 - error SC1013: var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet - 7 | console.log('boot', helper()); - 8 | const { greet } = require('./lib.js'); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 9 | \ No newline at end of file + 8 | console.log('boot', helper()); + 9 | var lib = require('./lib.js'); + | ^~~~~~~~~~~~~~~~~~~~~~~~~ + 10 | \ No newline at end of file From ae91df908ec41876a8394375961c83c9bc1c12f9 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sat, 17 Oct 2026 21:53:01 -0700 Subject: [PATCH 2/3] Admit require cycles whose modules settle their exports first A require cycle was always refused (SC1016). Node answers a require of a module that is still evaluating with its exports as they stand, while the compiled bindings alias the final declarations; the two agree when every module of the cycle assigns all of its exports before its first statement that can run another module's code, every other use of its export object is a plain member read, and no module's top-level code can reach its binding of another member. Each module then initializes exactly as it would outside the cycle. This admits semver's comparator/range cycle ("hoisted class for cyclic dependency"); other cycles keep SC1016 with a reason naming the offending statement. The require reachability scan no longer treats a declaration's own name as a read (esbuild's `var X = class {...}` before its requires), treats a plain write of an export member as running only a setter published for it (on-finished's `module.exports.isFinished = isFinished`), and keeps the conservative scan for writes of __proto__, __esModule and members of a replaced export object. Evidence: corpus 6145 (the cycle), 6142 gains the esbuild layout; the npm-static harness compiles the published fs.realpath@1.0.0, on-finished@2.4.1 (with ee-first@1.1.1) and statuses@2.0.1 statically; program.test.ts pins the TDZ analysis through var bindings plus the cycle admission and refusals. Preflight/order baselines recorded for the new fixtures; a full re-record changed no other fixture. Dependencies and limitations docs describe the supported require forms. Co-Authored-By: Claude Opus 5.5 --- docs/content/docs/dependencies.mdx | 4 + docs/content/docs/limitations.mdx | 2 + .../language-features/commonjs-require.mdx | 1 + .../compiler/src/frontend/program.test.ts | 97 +++++- packages/compiler/src/frontend/program.ts | 256 ++++++++++++++- .../6140-cjs-require-tdz-throws/main.js.json | 7 + .../6141-cjs-require-tdz-checked/main.js.json | 8 + .../main.js.json | 13 + .../corpus/6143-cjs-require-json/main.js.json | 7 + .../main.js.json | 6 + .../6145-cjs-require-cycle/main.js.json | 9 + .../cjs-require-cycle/main.js.json | 2 +- .../cjs-require-tdz-unchecked/main.js.json | 7 + .../diagnostics/cjs-require-tdz/main.js.json | 6 +- .../bundle.js | 17 + .../6142-cjs-require-published-early/core.js | 10 + .../6142-cjs-require-published-early/main.js | 2 + .../6145-cjs-require-cycle/comparator.js | 29 ++ tests/corpus/6145-cjs-require-cycle/main.js | 15 + .../6145-cjs-require-cycle/package.json | 1 + tests/corpus/6145-cjs-require-cycle/range.js | 24 ++ tests/corpus/6145-cjs-require-cycle/util.js | 10 + tests/diagnostics/cjs-require-cycle/main.js | 6 +- tests/fixtures/npm-static/cjs-interop-cli.ts | 9 + .../npm-static/node_modules/ee-first/LICENSE | 22 ++ .../npm-static/node_modules/ee-first/index.js | 95 ++++++ .../node_modules/ee-first/package.json | 29 ++ .../node_modules/fs.realpath/LICENSE | 43 +++ .../node_modules/fs.realpath/index.js | 66 ++++ .../node_modules/fs.realpath/old.js | 303 ++++++++++++++++++ .../node_modules/fs.realpath/package.json | 26 ++ .../node_modules/on-finished/LICENSE | 23 ++ .../node_modules/on-finished/index.js | 234 ++++++++++++++ .../node_modules/on-finished/package.json | 39 +++ .../npm-static/node_modules/statuses/LICENSE | 23 ++ .../node_modules/statuses/codes.json | 65 ++++ .../npm-static/node_modules/statuses/index.js | 146 +++++++++ .../node_modules/statuses/package.json | 49 +++ .../cjs-require-cycle/main.js.txt | 2 +- tests/harness/npm-static.test.ts | 18 ++ 40 files changed, 1717 insertions(+), 14 deletions(-) create mode 100644 docs/content/limitations/language-features/commonjs-require.mdx create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json create mode 100644 tests/corpus/6142-cjs-require-published-early/bundle.js create mode 100644 tests/corpus/6142-cjs-require-published-early/core.js create mode 100644 tests/corpus/6145-cjs-require-cycle/comparator.js create mode 100644 tests/corpus/6145-cjs-require-cycle/main.js create mode 100644 tests/corpus/6145-cjs-require-cycle/package.json create mode 100644 tests/corpus/6145-cjs-require-cycle/range.js create mode 100644 tests/corpus/6145-cjs-require-cycle/util.js create mode 100644 tests/fixtures/npm-static/cjs-interop-cli.ts create mode 100644 tests/fixtures/npm-static/node_modules/ee-first/LICENSE create mode 100644 tests/fixtures/npm-static/node_modules/ee-first/index.js create mode 100644 tests/fixtures/npm-static/node_modules/ee-first/package.json create mode 100644 tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE create mode 100644 tests/fixtures/npm-static/node_modules/fs.realpath/index.js create mode 100644 tests/fixtures/npm-static/node_modules/fs.realpath/old.js create mode 100644 tests/fixtures/npm-static/node_modules/fs.realpath/package.json create mode 100644 tests/fixtures/npm-static/node_modules/on-finished/LICENSE create mode 100644 tests/fixtures/npm-static/node_modules/on-finished/index.js create mode 100644 tests/fixtures/npm-static/node_modules/on-finished/package.json create mode 100644 tests/fixtures/npm-static/node_modules/statuses/LICENSE create mode 100644 tests/fixtures/npm-static/node_modules/statuses/codes.json create mode 100644 tests/fixtures/npm-static/node_modules/statuses/index.js create mode 100644 tests/fixtures/npm-static/node_modules/statuses/package.json diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx index f939522217..5ecc144c60 100644 --- a/docs/content/docs/dependencies.mdx +++ b/docs/content/docs/dependencies.mdx @@ -69,6 +69,10 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated. +## CommonJS packages + +CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms. + ## Unsupported code inside a package A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses. diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx index ed2c5b1ec3..544bd2b2dd 100644 --- a/docs/content/docs/limitations.mdx +++ b/docs/content/docs/limitations.mdx @@ -32,6 +32,8 @@ title: "Limitations" ../limitations/language-features/dynamic-imports.mdx +../limitations/language-features/commonjs-require.mdx + ### Types and class definitions ../limitations/types-and-class-definitions/bigint.mdx diff --git a/docs/content/limitations/language-features/commonjs-require.mdx b/docs/content/limitations/language-features/commonjs-require.mdx new file mode 100644 index 0000000000..5784994103 --- /dev/null +++ b/docs/content/limitations/language-features/commonjs-require.mdx @@ -0,0 +1 @@ +- **CommonJS `require`:** Literal `require()` calls of compiled modules, supported builtins, and JSON documents compile. Reading a top-level `const` or `let` require binding before its `require` runs throws the same `ReferenceError` as Node.js. A `var` binding that code above its `require` can reach is unsupported, and so is a read that compiles to no checkable expression, such as exporting the binding before the `require`. JSON documents are parsed at build time, and every `require()` of a document returns the same object. Destructuring the exports of a module supports plain and renamed properties, plus nested patterns for builtin sub-modules such as `util.types`. Any pattern works on a JSON document. Defaults, rest elements, and nested patterns over other modules are unsupported. A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call. Other require cycles are rejected with `SC1016`. diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts index 33abf6d679..8fec2f486f 100644 --- a/packages/compiler/src/frontend/program.test.ts +++ b/packages/compiler/src/frontend/program.test.ts @@ -126,10 +126,12 @@ test.for([ ["replacement export property", "module.exports = read; module.exports.name = read;"], ["non-export assignment chain", "const box = {}; exports.alias = box.read = read;"], ])("require still refuses an early read through %s", ([, early]) => { + // A var binding keeps the fence (Node reads undefined there); a const + // compiles with initialization checks instead, from the same analysis. const diagnostics = requireOrderDiagnostics(` exports.read = read; ${early} -const dep = require('./dep.cjs'); +var dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -140,13 +142,43 @@ function read() { return dep.value; } test("publishing a require binding itself still reads it before initialization", () => { const diagnostics = requireOrderDiagnostics(` exports.read = read; -const { read } = require('./dep.cjs'); +var { read } = require('./dep.cjs'); `); expect( diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'read'")), ).toBe(true); }); +test("an early-reachable const require binding compiles with initialization checks", () => { + // Preflight admits it; the lowering checks every reference that can run + // before the require (require-initialization.ts). + expect( + requireOrderDiagnostics(` +exports.read = read; +read(); +const dep = require('./dep.cjs'); +function read() { return dep.value; } +`), + ).toEqual([]); +}); + +test.for([ + ["a class whose methods read it", "class Reader { read() { return dep.value; } }"], + ["a stored class expression", "var Reader = class { read() { return dep.value; } };"], + ["a later-job callback", "setTimeout(() => dep.value, 0);"], + ["a published getter", "Object.defineProperty(exports, 'value', { get: () => dep.value });"], + ["a property stored on a published function", "module.exports = read; read.again = again;"], +])("code that cannot run before the require does not reach it: %s", ([, early]) => { + expect( + requireOrderDiagnostics(` +${early} +var dep = require('./dep.cjs'); +function read() { return dep.value; } +function again() { return dep.value; } +`), + ).toEqual([]); +}); + test.for([ "var Object = null; Object.defineProperty(exports, '__esModule', { value: true }); exports.read = read;", "var exports = null; exports.read = read;", @@ -154,7 +186,7 @@ test.for([ ])("source bindings cannot masquerade as a CommonJS prologue: %s", (prefix) => { const diagnostics = requireOrderDiagnostics(` ${prefix} -const dep = require('./dep.cjs'); +var dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -165,7 +197,7 @@ function read() { return dep.value; } test("an earlier declarator can call an export before the require initializes", () => { const diagnostics = requireOrderDiagnostics(` exports.read = read; -const before = read(), dep = require('./dep.cjs'); +var before = read(), dep = require('./dep.cjs'); function read() { return dep.value; } `); expect( @@ -183,6 +215,63 @@ function read() { return dep.value; } ).toEqual([]); }); +test("a member added to a replaced export does not run the replacement (on-finished)", () => { + expect( + requireOrderDiagnostics(` +module.exports = read; +module.exports.check = check; +var hooks = probe(); +var dep = require('./dep.cjs'); +function read() { return dep.value; } +function check() { return 1; } +function probe() { return check(); } +`), + ).toEqual([]); +}); + +test("a require cycle whose modules export first and use each other only later is admitted", () => { + expect( + requireOrderDiagnostics( + ` +const ANY = Symbol('ANY'); +class Main { dep() { return new Dep(); } } +module.exports = Main; +const Dep = require('./dep.cjs'); +`, + "class Dep { main() { return new Main(); } }\nmodule.exports = Dep;\nconst Main = require('./main.cjs');\n", + ), + ).toEqual([]); +}); + +test.for([ + [ + "an export after the require", + "const Main = require('./main.cjs');\nmodule.exports = { Dep: 1 };\n", + ], + [ + "a top-level use of the cycle binding", + "module.exports = 1;\nconst Main = require('./main.cjs');\nnew Main();\n", + ], + [ + "an escaping export object", + "module.exports = 1;\nconst Main = require('./main.cjs');\nObject.assign(exports, {});\n", + ], + [ + "a call before the export", + "setup();\nmodule.exports = 1;\nconst Main = require('./main.cjs');\nfunction setup() {}\n", + ], +])("a require cycle keeps SC1016 for %s", ([, dependency]) => { + const diagnostics = requireOrderDiagnostics( + ` +class Main {} +module.exports = Main; +const dep = require('./dep.cjs'); +`, + dependency, + ); + expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true); +}); + test("function publication does not admit CommonJS cycles", () => { const diagnostics = requireOrderDiagnostics( ` diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 82b2cb5e0b..df0cb673e2 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -1477,11 +1477,13 @@ export function requireBindingEarlyUse7( isWrapperName(e, "exports") || isModuleThis(e) || (isModuleExportsAccess(e) && isWrapperName(e.expression, "module")); + const replacesExports = stmts.some((st) => cjsExportAssignmentOf(st)?.kind === "table"); // Published values: by export name ("*" for a whole replacement), and by // the stored callable whose properties hold them. const exportLinks = new Map(); const ownerLinks = new Map(); const reachedKeys = new Set(); + const writtenKeys = new Set(); const reachedUnits = new Set(); const work: ts.Node[] = []; let hit: ts.Identifier | null = null; @@ -1521,6 +1523,24 @@ export function requireBindingEarlyUse7( } if (ts.isTypeOfExpression(parent)) return; if (ts.isPropertyAccessExpression(parent) && parent.expression === use) { + let target: ts.Node = parent; + while (ts.isParenthesizedExpression(target.parent)) target = target.parent; + const assignment = target.parent; + if ( + ts.isBinaryExpression(assignment) && + assignment.left === target && + assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + // A plain write reads no member. It can still run a setter: one + // published for that name (now or later), or a static one of a + // replacing class. + writtenKeys.add(parent.name.text); + for (const node of exportLinks.get(parent.name.text) ?? []) reachUnit(node); + for (const node of exportLinks.get("*") ?? []) { + if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) reachUnit(node); + } + return; + } reachKey(parent.name.text); return; } @@ -1566,6 +1586,8 @@ export function requireBindingEarlyUse7( ) { return undefined; } + // A declaration's own name reads nothing. + if (named !== undefined && declaredName7(named) === n) return undefined; if (cjs && isWrapperName(n, "exports")) { exportRead(n); return undefined; @@ -1674,7 +1696,13 @@ export function requireBindingEarlyUse7( } else break; } if (names.length === 0) return null; - if (isExportObject(e)) return { kind: "key", key: names[0]! }; + if (isExportObject(e)) { + // __proto__ and the interop marker are not plain data slots, and a + // member of a replaced export may be a read-only function property: + // those writes keep the conservative scan. + if (names[0] === "__proto__" || names[0] === "__esModule" || replacesExports) return null; + return { kind: "key", key: names[0]! }; + } if (ts.isIdentifier(e)) { const sym = symbolOf(e); if (sym !== undefined && units.has(sym) && !bindings.has(sym)) @@ -1684,7 +1712,13 @@ export function requireBindingEarlyUse7( }; const linkTo = (target: Target, node: ts.Node): void => { if (target.kind === "key") { - if (keyReached(target.key)) reachUnit(node); + const replacingClass = + target.key === "*" && + writtenKeys.size > 0 && + (ts.isClassDeclaration(node) || ts.isClassExpression(node)); + if (keyReached(target.key) || writtenKeys.has(target.key) || replacingClass) { + reachUnit(node); + } const list = exportLinks.get(target.key); if (list) list.push(node); else exportLinks.set(target.key, [node]); @@ -2777,8 +2811,19 @@ export function makeCycleAdmission( bindingUses.set(sf, uses); return uses; }; + // The CommonJS cluster verdict, same keying (cjsCycleRefusal7). + const cjsVerdict = new Map(); return (importer: ts.SourceFile, e: CycleEdge): string | null => { - if (e.stmt === undefined) return "the cycle closes through a require() edge"; + if (e.stmt === undefined) { + if (!index.has(importer)) strongconnect(importer); + const comp = sccOf.get(e.dep); + if (comp === undefined || comp.length < 2 || !comp.includes(importer)) { + return "the cycle closes through a require() edge"; + } + if (!cjsVerdict.has(comp)) cjsVerdict.set(comp, cjsCycleRefusal7(program, comp, lineOf)); + const reason = cjsVerdict.get(comp) ?? null; + return reason === null ? null : `the cycle closes through a require() edge: ${reason}`; + } // Cheap per-edge admission: nothing readable binds through the edge. if (ts.isImportDeclaration(e.stmt)) { const clause = e.stmt.importClause; @@ -2824,6 +2869,211 @@ export function makeCycleAdmission( }; } +/** The reason a strongly-connected cluster of CommonJS modules cannot be + * initialized exactly through its require() cycle, or null when it can. + * Node answers a require of a module that is still evaluating with that + * module's exports object as it stands, while the compiled bindings alias + * the exporter's final declarations. The two agree when: + * - every member settles its exports before anything can enter the + * cycle: all of its export statements come before its first top-level + * statement that can run another module's code (a require, or anything + * but literals, plain reads, function and class values, and + * standard-library globals called with literal arguments), and every + * other use of its export object is a plain member read; and + * - no member's top-level code can reach its binding of another member, + * so no member runs another member's code while the cluster is still + * initializing. Each module then initializes exactly as it would + * outside the cycle, and its own require bindings keep the acyclic + * analysis (requireEarlyReference). + * Other cycles keep the SC1016 fence. */ +function cjsCycleRefusal7( + program: ts.Program, + comp: readonly ts.SourceFile[], + lineOf: (node: ts.Node) => string, +): string | null { + const members = new Set(comp); + const builtinRoot = (e: ts.Expression): boolean => { + while (ts.isPropertyAccessExpression(e)) e = e.expression; + return ts.isIdentifier(e) && !sourceBinding7(program, e); + }; + const literal = (e: ts.Node): boolean => + ts.isStringLiteralLike(e) || + ts.isNumericLiteral(e) || + ts.isRegularExpressionLiteral(e) || + e.kind === ts.SyntaxKind.TrueKeyword || + e.kind === ts.SyntaxKind.FalseKeyword || + e.kind === ts.SyntaxKind.NullKeyword; + /** An expression whose evaluation runs no user code and requires + * nothing: literals, plain reads, function and class values, literal + * containers of those, and standard-library globals called with + * literal arguments. */ + const inertExpr = (e: ts.Node): boolean => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + if (literal(e) || ts.isIdentifier(e)) return true; + if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true; + if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart); + if (ts.isObjectLiteralExpression(e)) { + return e.properties.every((p) => + ts.isShorthandPropertyAssignment(p) + ? true + : ts.isPropertyAssignment(p) + ? !ts.isComputedPropertyName(p.name) && inertExpr(p.initializer) + : (ts.isMethodDeclaration(p) || + ts.isGetAccessorDeclaration(p) || + ts.isSetAccessorDeclaration(p)) && + !ts.isComputedPropertyName(p.name), + ); + } + if (ts.isArrayLiteralExpression(e)) { + return e.elements.every((el) => !ts.isSpreadElement(el) && inertExpr(el)); + } + if (ts.isPropertyAccessExpression(e)) return builtinRoot(e); + if (ts.isVoidExpression(e)) return literal(e.expression); + if (ts.isCallExpression(e) || ts.isNewExpression(e)) { + return ( + requireSpecOf7(e) === null && + builtinRoot(e.expression) && + (e.arguments ?? []).every(literal) + ); + } + return false; + }; + const inertPart = (part: ts.Node): boolean => { + if (ts.isHeritageClause(part)) return part.types.every((t) => inertExpr(t.expression)); + if (ts.isComputedPropertyName(part)) return inertExpr(part.expression); + return ts.isExpression(part) && inertExpr(part); + }; + const exportObject = (e: ts.Expression): boolean => + (ts.isIdentifier(e) && e.text === "exports" && !sourceBinding7(program, e)) || + (isModuleExportsAccess(e) && + ts.isIdentifier(e.expression) && + !sourceBinding7(program, e.expression)); + /** A top-level statement that only publishes inert values. */ + const exportStatement = (stmt: ts.Statement): boolean => { + if (!ts.isExpressionStatement(stmt)) return false; + if (isEsModuleStamp(stmt.expression)) return true; + const cjs = cjsExportAssignmentOf(stmt); + if (cjs !== null) { + let value: ts.Expression = cjs.expr; + while ( + ts.isBinaryExpression(value) && + value.operatorToken.kind === ts.SyntaxKind.EqualsToken + ) { + const left = value.left; + if ( + !isModuleExportsAccess(left) && + !(ts.isPropertyAccessExpression(left) && exportObject(left.expression)) + ) + return false; + value = value.right; + } + return inertExpr(value); + } + const call = stmt.expression; + return ( + ts.isCallExpression(call) && + ts.isPropertyAccessExpression(call.expression) && + ts.isIdentifier(call.expression.expression) && + call.expression.expression.text === "Object" && + !sourceBinding7(program, call.expression.expression) && + call.expression.name.text === "defineProperty" && + call.arguments.length === 3 && + exportObject(call.arguments[0]!) && + ts.isStringLiteralLike(call.arguments[1]!) && + inertExpr(call.arguments[2]!) + ); + }; + for (const m of comp) { + if (!isCjsJsFile7(m, program)) { + return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`; + } + const stmts = m.statements; + // The first statement that can run another module's code. + let firstEntry = stmts.length; + for (let i = 0; i < stmts.length; i++) { + const st = stmts[i]!; + const inert = + ts.isFunctionDeclaration(st) || + ts.isEmptyStatement(st) || + (ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) || + (ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) || + (ts.isVariableStatement(st) && + st.declarationList.declarations.every( + (d) => + ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)), + )) || + exportStatement(st); + if (!inert) { + firstEntry = i; + break; + } + } + // Every other use of the export object must be a plain member read: + // anything else can change what a module of the cycle observes. + let stray: ts.Node | null = null; + const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement)); + ts.walkPreorder(m, (n) => { + if (stray !== null) return "stop"; + if (ts.isStatement(n) && settled.has(n)) return "skip"; + let use: ts.Expression | null = null; + if (n.kind === ts.SyntaxKind.ThisKeyword) { + let moduleThis = true; + for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) { + if ( + (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) || + ts.isClassDeclaration(p) || + ts.isClassExpression(p) + ) { + moduleThis = false; + break; + } + } + if (moduleThis) use = n as ts.Expression; + } else if ( + ts.isIdentifier(n) && + (n.text === "exports" || n.text === "module") && + !(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) && + !sourceBinding7(program, n) + ) { + if (n.text === "exports") use = n; + else if (!ts.isPropertyAccessExpression(n.parent) || n.parent.expression !== n) stray = n; + else if (n.parent.name.text === "exports") use = n.parent; + } + if (use === null) return undefined; + const parent = use.parent; + const read = + ts.isPropertyAccessExpression(parent) && + parent.expression === use && + !( + ts.isBinaryExpression(parent.parent) && + parent.parent.left === parent && + parent.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + parent.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment + ) && + !(ts.isCallExpression(parent.parent) && parent.parent.expression === parent) && + !ts.isDeleteExpression(parent.parent); + if (!read) stray = use; + return undefined; + }); + if (stray !== null) { + return `${m.fileName} uses its exports at ${lineOf(stray)} outside the export statements that precede its first require, so a module of the cycle can observe them before they settle`; + } + // Bindings of other members must stay unreachable from top-level code. + for (const st of stmts) { + for (const req of requiresOf7(st)) { + if (req.decl === null) continue; + const dep = resolveImport7(program, m, req.spec, "require"); + if (dep === null || !members.has(dep)) continue; + const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, []); + if (reached !== null) { + return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`; + } + } + } + } + return null; +} + /* ── exact cyclic initialization ───────────────────────────────────────── * Cycles that fail the declaration-only rule still evaluate exactly like * Node when their shape is representable: the guarded %init calls already diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json new file mode 100644 index 0000000000..db768b31be --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6140-cjs-require-tdz-throws/lib.js", + "/tests/corpus/6140-cjs-require-tdz-throws/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json new file mode 100644 index 0000000000..b074bf18d6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json @@ -0,0 +1,8 @@ +{ + "order": [ + "/tests/corpus/6141-cjs-require-tdz-checked/lib.js", + "/tests/corpus/6141-cjs-require-tdz-checked/counter.js", + "/tests/corpus/6141-cjs-require-tdz-checked/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json new file mode 100644 index 0000000000..55720ef1a4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json @@ -0,0 +1,13 @@ +{ + "order": [ + "/tests/corpus/6142-cjs-require-published-early/parse-options.js", + "/tests/corpus/6142-cjs-require-published-early/debug.js", + "/tests/corpus/6142-cjs-require-published-early/comparator.js", + "/tests/corpus/6142-cjs-require-published-early/helpers.js", + "/tests/corpus/6142-cjs-require-published-early/glob.js", + "/tests/corpus/6142-cjs-require-published-early/core.js", + "/tests/corpus/6142-cjs-require-published-early/bundle.js", + "/tests/corpus/6142-cjs-require-published-early/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json new file mode 100644 index 0000000000..92fe169c31 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6143-cjs-require-json/counter.js", + "/tests/corpus/6143-cjs-require-json/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json new file mode 100644 index 0000000000..df78ca9ed9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6144-cjs-require-builtin-nested/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json new file mode 100644 index 0000000000..3e8122d1f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json @@ -0,0 +1,9 @@ +{ + "order": [ + "/tests/corpus/6145-cjs-require-cycle/range.js", + "/tests/corpus/6145-cjs-require-cycle/util.js", + "/tests/corpus/6145-cjs-require-cycle/comparator.js", + "/tests/corpus/6145-cjs-require-cycle/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json index a54923bcfe..67d01f47e2 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json @@ -7,7 +7,7 @@ "diags": [ { "code": "SC1016", - "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet", + "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge: /tests/diagnostics/cjs-require-cycle/a.js uses its exports at /tests/diagnostics/cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet", "loc": { "file": "/tests/diagnostics/cjs-require-cycle/a.js", "start": 0, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json new file mode 100644 index 0000000000..aaeea4cf27 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/diagnostics/cjs-require-tdz-unchecked/lib.js", + "/tests/diagnostics/cjs-require-tdz-unchecked/main.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json index d15d404ae9..205a8cd6ee 100644 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json @@ -5,11 +5,11 @@ "diags": [ { "code": "SC1013", - "message": "require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet", + "message": "var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet", "loc": { "file": "/tests/diagnostics/cjs-require-tdz/main.js", - "start": 336, - "end": 367 + "start": 402, + "end": 427 }, "milestone": "later" } diff --git a/tests/corpus/6142-cjs-require-published-early/bundle.js b/tests/corpus/6142-cjs-require-published-early/bundle.js new file mode 100644 index 0000000000..c123872197 --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/bundle.js @@ -0,0 +1,17 @@ +'use strict'; + +// The esbuild CommonJS layout: stored class expressions and the export +// table come before the var requires their methods use. Publishing the +// classes runs none of their methods. +const Provider = class { + /** @param {string} name */ + constructor(name) { + this.name = name; + } + describe() { + return core.label(this.name); + } +}; +const REASON = 'default'; +module.exports = { Provider, REASON }; +var core = require('./core.js'); diff --git a/tests/corpus/6142-cjs-require-published-early/core.js b/tests/corpus/6142-cjs-require-published-early/core.js new file mode 100644 index 0000000000..9e3a9a565c --- /dev/null +++ b/tests/corpus/6142-cjs-require-published-early/core.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('core: init'); + +/** @param {string} name */ +function label(name) { + return `<${name}>`; +} + +module.exports = { label }; diff --git a/tests/corpus/6142-cjs-require-published-early/main.js b/tests/corpus/6142-cjs-require-published-early/main.js index 9182f1cf5b..310dacdb01 100644 --- a/tests/corpus/6142-cjs-require-published-early/main.js +++ b/tests/corpus/6142-cjs-require-published-early/main.js @@ -6,8 +6,10 @@ console.log('main: start'); const Comparator = require('./comparator.js'); const glob = require('./glob.js'); +const { Provider, REASON } = require('./bundle.js'); const c = new Comparator(' a.txt ', { loose: true }); console.log(c.mode, c.text, c.test('/x/a.txt'), Comparator.ANY.toString()); console.log(new Comparator('b').mode, Comparator.describe()); console.log(glob('a//b'), glob.sync('c//d')); +console.log(new Provider('custom').describe(), REASON); diff --git a/tests/corpus/6145-cjs-require-cycle/comparator.js b/tests/corpus/6145-cjs-require-cycle/comparator.js new file mode 100644 index 0000000000..1f0d4eb9d2 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/comparator.js @@ -0,0 +1,29 @@ +'use strict'; + +// semver's layout: the class and its export come first ("hoisted class for +// cyclic dependency"), the requires — one of them closing the cycle with +// range.js — after it. When range.js requires this module mid-evaluation, +// Node hands it the exports as they stand: already the class. +const ANY = Symbol('ANY'); + +class Comparator { + /** @param {string} value */ + constructor(value) { + this.value = value; + } + + /** @param {string} other */ + within(other) { + return new Range(other).has(this.value); + } + + static get ANY() { + return ANY; + } +} + +module.exports = Comparator; + +const Range = require('./range.js'); +const { trim } = require('./util.js'); +console.log('comparator: loaded', trim(' done ')); diff --git a/tests/corpus/6145-cjs-require-cycle/main.js b/tests/corpus/6145-cjs-require-cycle/main.js new file mode 100644 index 0000000000..90cbc88247 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/main.js @@ -0,0 +1,15 @@ +// A require cycle (comparator.js ↔ range.js) whose modules settle their +// exports before they require anything, and use each other only inside +// methods: the cycle evaluates in Node's order and each binding is the +// other module's class. +'use strict'; + +console.log('main: start'); +const Comparator = require('./comparator.js'); +const Range = require('./range.js'); + +const range = new Range('1.0.0 2.0.0'); +console.log(range.has('2.0.0'), range.has('3.0.0')); +const first = range.first(); +console.log(first instanceof Comparator, first.value, first.within('2.0.0 1.0.0')); +console.log(new Comparator('9').within('1 2'), String(Comparator.ANY)); diff --git a/tests/corpus/6145-cjs-require-cycle/package.json b/tests/corpus/6145-cjs-require-cycle/package.json new file mode 100644 index 0000000000..a3c15a7a63 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/package.json @@ -0,0 +1 @@ +{ "type": "commonjs" } diff --git a/tests/corpus/6145-cjs-require-cycle/range.js b/tests/corpus/6145-cjs-require-cycle/range.js new file mode 100644 index 0000000000..ab71e4a52d --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/range.js @@ -0,0 +1,24 @@ +'use strict'; + +const SEPARATOR = /\s+/; + +class Range { + /** @param {string} spec */ + constructor(spec) { + this.parts = spec.split(SEPARATOR).map((part) => new Comparator(part)); + } + + /** @param {string} value */ + has(value) { + return this.parts.some((part) => part.value === value); + } + + first() { + return this.parts[0]; + } +} + +module.exports = Range; + +const Comparator = require('./comparator.js'); +console.log('range: loaded'); diff --git a/tests/corpus/6145-cjs-require-cycle/util.js b/tests/corpus/6145-cjs-require-cycle/util.js new file mode 100644 index 0000000000..4e13760cd4 --- /dev/null +++ b/tests/corpus/6145-cjs-require-cycle/util.js @@ -0,0 +1,10 @@ +'use strict'; + +console.log('util: loaded'); + +/** @param {string} s */ +function trim(s) { + return s.trim(); +} + +module.exports = { trim }; diff --git a/tests/diagnostics/cjs-require-cycle/main.js b/tests/diagnostics/cjs-require-cycle/main.js index 619117b910..376aa0a363 100644 --- a/tests/diagnostics/cjs-require-cycle/main.js +++ b/tests/diagnostics/cjs-require-cycle/main.js @@ -1,6 +1,8 @@ -// A require cycle: Node hands B the PARTIAL exports of A mid-init, a +// A require cycle whose module exports only after requiring into the +// cycle: Node hands B the PARTIAL exports of A mid-init, a // partially-initialized-module observation the export-global model cannot -// represent — cycles keep the named SC1016 fence. +// represent — the cycle keeps the named SC1016 fence. (A cycle whose +// modules export first compiles: tests/corpus/6145-cjs-require-cycle.) 'use strict'; console.log('main: start'); diff --git a/tests/fixtures/npm-static/cjs-interop-cli.ts b/tests/fixtures/npm-static/cjs-interop-cli.ts new file mode 100644 index 0000000000..2b9a88f635 --- /dev/null +++ b/tests/fixtures/npm-static/cjs-interop-cli.ts @@ -0,0 +1,9 @@ +// npm-static pilots: real CommonJS packages that preflight refused before. +// fs.realpath@1.0.0 and on-finished@2.4.1 assign module.exports and run +// code before they require their dependencies (each binding is reachable +// only after its require ran); statuses@2.0.1 requires its codes.json. +import realpath from "fs.realpath"; +import onFinished from "on-finished"; +import status from "statuses"; + +console.log(typeof realpath.realpathSync, typeof onFinished, typeof status); diff --git a/tests/fixtures/npm-static/node_modules/ee-first/LICENSE b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE new file mode 100644 index 0000000000..a7ae8ee9b8 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong me@jongleberry.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/ee-first/index.js b/tests/fixtures/npm-static/node_modules/ee-first/index.js new file mode 100644 index 0000000000..501287cd3b --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/index.js @@ -0,0 +1,95 @@ +/*! + * ee-first + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = first + +/** + * Get the first event in a set of event emitters and event pairs. + * + * @param {array} stuff + * @param {function} done + * @public + */ + +function first(stuff, done) { + if (!Array.isArray(stuff)) + throw new TypeError('arg must be an array of [ee, events...] arrays') + + var cleanups = [] + + for (var i = 0; i < stuff.length; i++) { + var arr = stuff[i] + + if (!Array.isArray(arr) || arr.length < 2) + throw new TypeError('each array member must be [ee, events...]') + + var ee = arr[0] + + for (var j = 1; j < arr.length; j++) { + var event = arr[j] + var fn = listener(event, callback) + + // listen to the event + ee.on(event, fn) + // push this listener to the list of cleanups + cleanups.push({ + ee: ee, + event: event, + fn: fn, + }) + } + } + + function callback() { + cleanup() + done.apply(null, arguments) + } + + function cleanup() { + var x + for (var i = 0; i < cleanups.length; i++) { + x = cleanups[i] + x.ee.removeListener(x.event, x.fn) + } + } + + function thunk(fn) { + done = fn + } + + thunk.cancel = cleanup + + return thunk +} + +/** + * Create the event listener. + * @private + */ + +function listener(event, done) { + return function onevent(arg1) { + var args = new Array(arguments.length) + var ee = this + var err = event === 'error' + ? arg1 + : null + + // copy args to prevent arguments escaping scope + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i] + } + + done(err, ee, event, args) + } +} diff --git a/tests/fixtures/npm-static/node_modules/ee-first/package.json b/tests/fixtures/npm-static/node_modules/ee-first/package.json new file mode 100644 index 0000000000..b6d0b7d675 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/ee-first/package.json @@ -0,0 +1,29 @@ +{ + "name": "ee-first", + "description": "return the first event in a set of ee/event pairs", + "version": "1.1.1", + "author": { + "name": "Jonathan Ong", + "email": "me@jongleberry.com", + "url": "http://jongleberry.com", + "twitter": "https://twitter.com/jongleberry" + }, + "contributors": [ + "Douglas Christopher Wilson " + ], + "license": "MIT", + "repository": "jonathanong/ee-first", + "devDependencies": { + "istanbul": "0.3.9", + "mocha": "2.2.5" + }, + "files": [ + "index.js", + "LICENSE" + ], + "scripts": { + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + } +} diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE new file mode 100644 index 0000000000..5bd884c252 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE @@ -0,0 +1,43 @@ +The ISC License + +Copyright (c) Isaac Z. Schlueter and Contributors + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +---- + +This library bundles a version of the `fs.realpath` and `fs.realpathSync` +methods from Node.js v0.10 under the terms of the Node.js MIT license. + +Node's license follows, also included at the header of `old.js` which contains +the licensed code: + + Copyright Joyent, Inc. and other Node contributors. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/index.js b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js new file mode 100644 index 0000000000..b09c7c7e63 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js @@ -0,0 +1,66 @@ +module.exports = realpath +realpath.realpath = realpath +realpath.sync = realpathSync +realpath.realpathSync = realpathSync +realpath.monkeypatch = monkeypatch +realpath.unmonkeypatch = unmonkeypatch + +var fs = require('fs') +var origRealpath = fs.realpath +var origRealpathSync = fs.realpathSync + +var version = process.version +var ok = /^v[0-5]\./.test(version) +var old = require('./old.js') + +function newError (er) { + return er && er.syscall === 'realpath' && ( + er.code === 'ELOOP' || + er.code === 'ENOMEM' || + er.code === 'ENAMETOOLONG' + ) +} + +function realpath (p, cache, cb) { + if (ok) { + return origRealpath(p, cache, cb) + } + + if (typeof cache === 'function') { + cb = cache + cache = null + } + origRealpath(p, cache, function (er, result) { + if (newError(er)) { + old.realpath(p, cache, cb) + } else { + cb(er, result) + } + }) +} + +function realpathSync (p, cache) { + if (ok) { + return origRealpathSync(p, cache) + } + + try { + return origRealpathSync(p, cache) + } catch (er) { + if (newError(er)) { + return old.realpathSync(p, cache) + } else { + throw er + } + } +} + +function monkeypatch () { + fs.realpath = realpath + fs.realpathSync = realpathSync +} + +function unmonkeypatch () { + fs.realpath = origRealpath + fs.realpathSync = origRealpathSync +} diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/old.js b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js new file mode 100644 index 0000000000..b40305e73f --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js @@ -0,0 +1,303 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var pathModule = require('path'); +var isWindows = process.platform === 'win32'; +var fs = require('fs'); + +// JavaScript implementation of realpath, ported from node pre-v6 + +var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG); + +function rethrow() { + // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and + // is fairly slow to generate. + var callback; + if (DEBUG) { + var backtrace = new Error; + callback = debugCallback; + } else + callback = missingCallback; + + return callback; + + function debugCallback(err) { + if (err) { + backtrace.message = err.message; + err = backtrace; + missingCallback(err); + } + } + + function missingCallback(err) { + if (err) { + if (process.throwDeprecation) + throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs + else if (!process.noDeprecation) { + var msg = 'fs: missing callback ' + (err.stack || err.message); + if (process.traceDeprecation) + console.trace(msg); + else + console.error(msg); + } + } + } +} + +function maybeCallback(cb) { + return typeof cb === 'function' ? cb : rethrow(); +} + +var normalize = pathModule.normalize; + +// Regexp that finds the next partion of a (partial) path +// result is [base_with_slash, base], e.g. ['somedir/', 'somedir'] +if (isWindows) { + var nextPartRe = /(.*?)(?:[\/\\]+|$)/g; +} else { + var nextPartRe = /(.*?)(?:[\/]+|$)/g; +} + +// Regex to find the device root, including trailing slash. E.g. 'c:\\'. +if (isWindows) { + var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/; +} else { + var splitRootRe = /^[\/]*/; +} + +exports.realpathSync = function realpathSync(p, cache) { + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return cache[p]; + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstatSync(base); + knownHard[base] = true; + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + // NB: p.length changes. + while (pos < p.length) { + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + continue; + } + + var resolvedLink; + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // some known symbolic link. no need to stat again. + resolvedLink = cache[base]; + } else { + var stat = fs.lstatSync(base); + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + continue; + } + + // read the link if it wasn't read before + // dev/ino always return 0 on windows, so skip the check. + var linkTarget = null; + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + linkTarget = seenLinks[id]; + } + } + if (linkTarget === null) { + fs.statSync(base); + linkTarget = fs.readlinkSync(base); + } + resolvedLink = pathModule.resolve(previous, linkTarget); + // track this, if given a cache. + if (cache) cache[base] = resolvedLink; + if (!isWindows) seenLinks[id] = linkTarget; + } + + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } + + if (cache) cache[original] = p; + + return p; +}; + + +exports.realpath = function realpath(p, cache, cb) { + if (typeof cb !== 'function') { + cb = maybeCallback(cache); + cache = null; + } + + // make p is absolute + p = pathModule.resolve(p); + + if (cache && Object.prototype.hasOwnProperty.call(cache, p)) { + return process.nextTick(cb.bind(null, null, cache[p])); + } + + var original = p, + seenLinks = {}, + knownHard = {}; + + // current character position in p + var pos; + // the partial path so far, including a trailing slash if any + var current; + // the partial path without a trailing slash (except when pointing at a root) + var base; + // the partial path scanned in the previous round, with slash + var previous; + + start(); + + function start() { + // Skip over roots + var m = splitRootRe.exec(p); + pos = m[0].length; + current = m[0]; + base = m[0]; + previous = ''; + + // On windows, check that the root exists. On unix there is no need. + if (isWindows && !knownHard[base]) { + fs.lstat(base, function(err) { + if (err) return cb(err); + knownHard[base] = true; + LOOP(); + }); + } else { + process.nextTick(LOOP); + } + } + + // walk down the path, swapping out linked pathparts for their real + // values + function LOOP() { + // stop if scanned past end of path + if (pos >= p.length) { + if (cache) cache[original] = p; + return cb(null, p); + } + + // find the next part + nextPartRe.lastIndex = pos; + var result = nextPartRe.exec(p); + previous = current; + current += result[0]; + base = previous + result[1]; + pos = nextPartRe.lastIndex; + + // continue if not a symlink + if (knownHard[base] || (cache && cache[base] === base)) { + return process.nextTick(LOOP); + } + + if (cache && Object.prototype.hasOwnProperty.call(cache, base)) { + // known symbolic link. no need to stat again. + return gotResolvedLink(cache[base]); + } + + return fs.lstat(base, gotStat); + } + + function gotStat(err, stat) { + if (err) return cb(err); + + // if not a symlink, skip to the next path part + if (!stat.isSymbolicLink()) { + knownHard[base] = true; + if (cache) cache[base] = base; + return process.nextTick(LOOP); + } + + // stat & read the link if not read before + // call gotTarget as soon as the link target is known + // dev/ino always return 0 on windows, so skip the check. + if (!isWindows) { + var id = stat.dev.toString(32) + ':' + stat.ino.toString(32); + if (seenLinks.hasOwnProperty(id)) { + return gotTarget(null, seenLinks[id], base); + } + } + fs.stat(base, function(err) { + if (err) return cb(err); + + fs.readlink(base, function(err, target) { + if (!isWindows) seenLinks[id] = target; + gotTarget(err, target); + }); + }); + } + + function gotTarget(err, target, base) { + if (err) return cb(err); + + var resolvedLink = pathModule.resolve(previous, target); + if (cache) cache[base] = resolvedLink; + gotResolvedLink(resolvedLink); + } + + function gotResolvedLink(resolvedLink) { + // resolve the link, then start over + p = pathModule.resolve(resolvedLink, p.slice(pos)); + start(); + } +}; diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/package.json b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json new file mode 100644 index 0000000000..3edc57d21c --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json @@ -0,0 +1,26 @@ +{ + "name": "fs.realpath", + "version": "1.0.0", + "description": "Use node's fs.realpath, but fall back to the JS implementation if the native one fails", + "main": "index.js", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "test": "tap test/*.js --cov" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/isaacs/fs.realpath.git" + }, + "keywords": [ + "realpath", + "fs", + "polyfill" + ], + "author": "Isaac Z. Schlueter (http://blog.izs.me/)", + "license": "ISC", + "files": [ + "old.js", + "index.js" + ] +} diff --git a/tests/fixtures/npm-static/node_modules/on-finished/LICENSE b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE new file mode 100644 index 0000000000..5931fd23ea --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE @@ -0,0 +1,23 @@ +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/on-finished/index.js b/tests/fixtures/npm-static/node_modules/on-finished/index.js new file mode 100644 index 0000000000..e68df7bde3 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/index.js @@ -0,0 +1,234 @@ +/*! + * on-finished + * Copyright(c) 2013 Jonathan Ong + * Copyright(c) 2014 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module exports. + * @public + */ + +module.exports = onFinished +module.exports.isFinished = isFinished + +/** + * Module dependencies. + * @private + */ + +var asyncHooks = tryRequireAsyncHooks() +var first = require('ee-first') + +/** + * Variables. + * @private + */ + +/* istanbul ignore next */ +var defer = typeof setImmediate === 'function' + ? setImmediate + : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) } + +/** + * Invoke callback when the response has finished, useful for + * cleaning up resources afterwards. + * + * @param {object} msg + * @param {function} listener + * @return {object} + * @public + */ + +function onFinished (msg, listener) { + if (isFinished(msg) !== false) { + defer(listener, null, msg) + return msg + } + + // attach the listener to the message + attachListener(msg, wrap(listener)) + + return msg +} + +/** + * Determine if message is already finished. + * + * @param {object} msg + * @return {boolean} + * @public + */ + +function isFinished (msg) { + var socket = msg.socket + + if (typeof msg.finished === 'boolean') { + // OutgoingMessage + return Boolean(msg.finished || (socket && !socket.writable)) + } + + if (typeof msg.complete === 'boolean') { + // IncomingMessage + return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable)) + } + + // don't know + return undefined +} + +/** + * Attach a finished listener to the message. + * + * @param {object} msg + * @param {function} callback + * @private + */ + +function attachFinishedListener (msg, callback) { + var eeMsg + var eeSocket + var finished = false + + function onFinish (error) { + eeMsg.cancel() + eeSocket.cancel() + + finished = true + callback(error) + } + + // finished on first message event + eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish) + + function onSocket (socket) { + // remove listener + msg.removeListener('socket', onSocket) + + if (finished) return + if (eeMsg !== eeSocket) return + + // finished on first socket event + eeSocket = first([[socket, 'error', 'close']], onFinish) + } + + if (msg.socket) { + // socket already assigned + onSocket(msg.socket) + return + } + + // wait for socket to be assigned + msg.on('socket', onSocket) + + if (msg.socket === undefined) { + // istanbul ignore next: node.js 0.8 patch + patchAssignSocket(msg, onSocket) + } +} + +/** + * Attach the listener to the message. + * + * @param {object} msg + * @return {function} + * @private + */ + +function attachListener (msg, listener) { + var attached = msg.__onFinished + + // create a private single listener with queue + if (!attached || !attached.queue) { + attached = msg.__onFinished = createListener(msg) + attachFinishedListener(msg, attached) + } + + attached.queue.push(listener) +} + +/** + * Create listener on message. + * + * @param {object} msg + * @return {function} + * @private + */ + +function createListener (msg) { + function listener (err) { + if (msg.__onFinished === listener) msg.__onFinished = null + if (!listener.queue) return + + var queue = listener.queue + listener.queue = null + + for (var i = 0; i < queue.length; i++) { + queue[i](err, msg) + } + } + + listener.queue = [] + + return listener +} + +/** + * Patch ServerResponse.prototype.assignSocket for node.js 0.8. + * + * @param {ServerResponse} res + * @param {function} callback + * @private + */ + +// istanbul ignore next: node.js 0.8 patch +function patchAssignSocket (res, callback) { + var assignSocket = res.assignSocket + + if (typeof assignSocket !== 'function') return + + // res.on('socket', callback) is broken in 0.8 + res.assignSocket = function _assignSocket (socket) { + assignSocket.call(this, socket) + callback(socket) + } +} + +/** + * Try to require async_hooks + * @private + */ + +function tryRequireAsyncHooks () { + try { + return require('async_hooks') + } catch (e) { + return {} + } +} + +/** + * Wrap function with async resource, if possible. + * AsyncResource.bind static method backported. + * @private + */ + +function wrap (fn) { + var res + + // create anonymous resource + if (asyncHooks.AsyncResource) { + res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn') + } + + // incompatible node.js + if (!res || !res.runInAsyncScope) { + return fn + } + + // return bound function + return res.runInAsyncScope.bind(res, fn, null) +} diff --git a/tests/fixtures/npm-static/node_modules/on-finished/package.json b/tests/fixtures/npm-static/node_modules/on-finished/package.json new file mode 100644 index 0000000000..644cd814b9 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/on-finished/package.json @@ -0,0 +1,39 @@ +{ + "name": "on-finished", + "description": "Execute a callback when a request closes, finishes, or errors", + "version": "2.4.1", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "jshttp/on-finished", + "dependencies": { + "ee-first": "1.1.1" + }, + "devDependencies": { + "eslint": "7.32.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.25.4", + "eslint-plugin-markdown": "2.2.1", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "5.2.0", + "eslint-plugin-standard": "4.1.0", + "mocha": "9.2.1", + "nyc": "15.1.0" + }, + "engines": { + "node": ">= 0.8" + }, + "files": [ + "HISTORY.md", + "LICENSE", + "index.js" + ], + "scripts": { + "lint": "eslint .", + "test": "mocha --reporter spec --bail --check-leaks test/", + "test-ci": "nyc --reporter=lcovonly --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test" + } +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/LICENSE b/tests/fixtures/npm-static/node_modules/statuses/LICENSE new file mode 100644 index 0000000000..28a3161820 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/LICENSE @@ -0,0 +1,23 @@ + +The MIT License (MIT) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/tests/fixtures/npm-static/node_modules/statuses/codes.json b/tests/fixtures/npm-static/node_modules/statuses/codes.json new file mode 100644 index 0000000000..1333ed10ba --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/codes.json @@ -0,0 +1,65 @@ +{ + "100": "Continue", + "101": "Switching Protocols", + "102": "Processing", + "103": "Early Hints", + "200": "OK", + "201": "Created", + "202": "Accepted", + "203": "Non-Authoritative Information", + "204": "No Content", + "205": "Reset Content", + "206": "Partial Content", + "207": "Multi-Status", + "208": "Already Reported", + "226": "IM Used", + "300": "Multiple Choices", + "301": "Moved Permanently", + "302": "Found", + "303": "See Other", + "304": "Not Modified", + "305": "Use Proxy", + "307": "Temporary Redirect", + "308": "Permanent Redirect", + "400": "Bad Request", + "401": "Unauthorized", + "402": "Payment Required", + "403": "Forbidden", + "404": "Not Found", + "405": "Method Not Allowed", + "406": "Not Acceptable", + "407": "Proxy Authentication Required", + "408": "Request Timeout", + "409": "Conflict", + "410": "Gone", + "411": "Length Required", + "412": "Precondition Failed", + "413": "Payload Too Large", + "414": "URI Too Long", + "415": "Unsupported Media Type", + "416": "Range Not Satisfiable", + "417": "Expectation Failed", + "418": "I'm a Teapot", + "421": "Misdirected Request", + "422": "Unprocessable Entity", + "423": "Locked", + "424": "Failed Dependency", + "425": "Too Early", + "426": "Upgrade Required", + "428": "Precondition Required", + "429": "Too Many Requests", + "431": "Request Header Fields Too Large", + "451": "Unavailable For Legal Reasons", + "500": "Internal Server Error", + "501": "Not Implemented", + "502": "Bad Gateway", + "503": "Service Unavailable", + "504": "Gateway Timeout", + "505": "HTTP Version Not Supported", + "506": "Variant Also Negotiates", + "507": "Insufficient Storage", + "508": "Loop Detected", + "509": "Bandwidth Limit Exceeded", + "510": "Not Extended", + "511": "Network Authentication Required" +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/index.js b/tests/fixtures/npm-static/node_modules/statuses/index.js new file mode 100644 index 0000000000..ea351c5535 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/index.js @@ -0,0 +1,146 @@ +/*! + * statuses + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2016 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict' + +/** + * Module dependencies. + * @private + */ + +var codes = require('./codes.json') + +/** + * Module exports. + * @public + */ + +module.exports = status + +// status code to message map +status.message = codes + +// status message (lower-case) to code map +status.code = createMessageToStatusCodeMap(codes) + +// array of status codes +status.codes = createStatusCodeList(codes) + +// status codes for redirects +status.redirect = { + 300: true, + 301: true, + 302: true, + 303: true, + 305: true, + 307: true, + 308: true +} + +// status codes for empty bodies +status.empty = { + 204: true, + 205: true, + 304: true +} + +// status codes for when you should retry the request +status.retry = { + 502: true, + 503: true, + 504: true +} + +/** + * Create a map of message to status code. + * @private + */ + +function createMessageToStatusCodeMap (codes) { + var map = {} + + Object.keys(codes).forEach(function forEachCode (code) { + var message = codes[code] + var status = Number(code) + + // populate map + map[message.toLowerCase()] = status + }) + + return map +} + +/** + * Create a list of all status codes. + * @private + */ + +function createStatusCodeList (codes) { + return Object.keys(codes).map(function mapCode (code) { + return Number(code) + }) +} + +/** + * Get the status code for given message. + * @private + */ + +function getStatusCode (message) { + var msg = message.toLowerCase() + + if (!Object.prototype.hasOwnProperty.call(status.code, msg)) { + throw new Error('invalid status message: "' + message + '"') + } + + return status.code[msg] +} + +/** + * Get the status message for given code. + * @private + */ + +function getStatusMessage (code) { + if (!Object.prototype.hasOwnProperty.call(status.message, code)) { + throw new Error('invalid status code: ' + code) + } + + return status.message[code] +} + +/** + * Get the status code. + * + * Given a number, this will throw if it is not a known status + * code, otherwise the code will be returned. Given a string, + * the string will be parsed for a number and return the code + * if valid, otherwise will lookup the code assuming this is + * the status message. + * + * @param {string|number} code + * @returns {number} + * @public + */ + +function status (code) { + if (typeof code === 'number') { + return getStatusMessage(code) + } + + if (typeof code !== 'string') { + throw new TypeError('code must be a number or string') + } + + // '403' + var n = parseInt(code, 10) + if (!isNaN(n)) { + return getStatusMessage(n) + } + + return getStatusCode(code) +} diff --git a/tests/fixtures/npm-static/node_modules/statuses/package.json b/tests/fixtures/npm-static/node_modules/statuses/package.json new file mode 100644 index 0000000000..8c3e719b50 --- /dev/null +++ b/tests/fixtures/npm-static/node_modules/statuses/package.json @@ -0,0 +1,49 @@ +{ + "name": "statuses", + "description": "HTTP status utility", + "version": "2.0.1", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "repository": "jshttp/statuses", + "license": "MIT", + "keywords": [ + "http", + "status", + "code" + ], + "files": [ + "HISTORY.md", + "index.js", + "codes.json", + "LICENSE" + ], + "devDependencies": { + "csv-parse": "4.14.2", + "eslint": "7.17.0", + "eslint-config-standard": "14.1.1", + "eslint-plugin-import": "2.22.1", + "eslint-plugin-markdown": "1.0.2", + "eslint-plugin-node": "11.1.0", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.1.0", + "mocha": "8.2.1", + "nyc": "15.1.0", + "raw-body": "2.4.1", + "stream-to-array": "2.3.0" + }, + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "build": "node scripts/build.js", + "fetch": "node scripts/fetch-apache.js && node scripts/fetch-iana.js && node scripts/fetch-nginx.js && node scripts/fetch-node.js", + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --reporter spec --check-leaks --bail test/", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=html --reporter=text npm test", + "update": "npm run fetch && npm run build", + "version": "node scripts/version-history.js && git add HISTORY.md" + } +} diff --git a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt index ad55be5244..8916284c3a 100644 --- a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt +++ b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt @@ -1,4 +1,4 @@ -cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet +cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge: cjs-require-cycle/a.js uses its exports at cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet 1 | 'use strict'; | ^ diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index 9324d1c7f6..a3d557b83d 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -878,6 +878,24 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { expect(nativeRes.exitCode).toBe(nodeRes.exitCode); }, 180_000); + // Real CommonJS packages whose require() placement (a binding required + // below code that only reaches it after the require ran) or JSON + // require preflight refused before. + test("export-first CommonJS packages and JSON requires compile statically", () => { + const { coverage } = analyze(join(pilotRoot, "cjs-interop-cli.ts"), { + npmStatic: ["fs.realpath", "on-finished", "ee-first", "statuses"], + }); + expect(coverage.preflightFailed).toBe(false); + expect( + [...(coverage.npmStatic ?? [])].sort((a, b) => a.package.localeCompare(b.package)), + ).toEqual([ + { package: "ee-first", status: "static" }, + { package: "fs.realpath", status: "static" }, + { package: "on-finished", status: "static" }, + { package: "statuses", status: "static" }, + ]); + }, 120_000); + // A package preflight failure stays visible as a package-specific refusal. test("a preflight-refused package reports its native refusal", () => { const { coverage } = analyze(join(fixturesRoot, "npm/divergent/lazybuiltin.ts"), { From 04297f3064af603d9c854d3d8b2637332fbe32c8 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Sun, 18 Oct 2026 06:56:24 -0700 Subject: [PATCH 3/3] Keep the preflight batching test on a fenced require binding The test asserts that the TDZ analysis batches symbol queries over the roots it scans, observed through an SC1013 refusal. Its const binding now compiles with initialization checks instead of a refusal, so the test uses a var binding, which keeps the fence and runs the same scan. Co-Authored-By: Claude Opus 5.5 --- packages/compiler/test/ts7/program-adapter.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts index 660394a6fd..e7479c7498 100644 --- a/packages/compiler/test/ts7/program-adapter.test.ts +++ b/packages/compiler/test/ts7/program-adapter.test.ts @@ -119,7 +119,7 @@ ${uses} return required.value + value; } before(); -const required = require("./dep.cjs"); +var required = require("./dep.cjs"); console.log(required.value); `, ); @@ -144,6 +144,8 @@ console.log(required.value); }) as Checker; program.checkerFacade = new CheckerFacade(proxy, { project: program.project.checker.project }); + // A var binding keeps the SC1013 fence (Node reads undefined there); a + // const would compile with initialization checks from the same scan. expect(checkPreflight(load).map((diag) => diag.code)).toContain("SC1013"); expect(calls.some(([arg]) => Array.isArray(arg) && arg.length > 24)).toBe(true); expect(calls.some(([arg]) => !Array.isArray(arg))).toBe(false);