diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index f93952221..5ecc144c6 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 ed2c5b1ec..544bd2b2d 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 000000000..578499410
--- /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/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index db36cdb52..f2bb89f6e 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "cade1b8e6e0c5c42fcc9",
+ "artifactVersion": "be8d5b094695385e34ea",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index dadc9e836..3c0350dc3 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -164,9 +164,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
- "partial": 17,
+ "partial": 18,
"refused": 0,
- "not-implemented": 22,
+ "not-implemented": 21,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -9312,9 +9312,9 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "detail": "No static compiler lowering is registered for this API yet.",
- "verification": "registry-gap"
+ "status": "partial",
+ "detail": "Implemented for the documented scriptc module-loader subset.",
+ "verification": "test-backed"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 1b7d28783..24821377e 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
- "audit-partial": 1143,
- "verify-gap": 2186,
+ "audit-partial": 1144,
+ "verify-gap": 2185,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -3941,12 +3941,16 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:child_process",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:child_process.child_process.exec",
+ "tests": [
+ "tests/corpus/5627-exec-callback.ts",
+ "tests/fixtures/node-types/child-exec.ts",
+ "tests/harness/project-config.test.ts"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 8463d1292..3587ae2cb 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -131,9 +131,9 @@
"apiEntries": 39,
"static": {
"supported": 0,
- "partial": 17,
+ "partial": 18,
"refused": 0,
- "not-implemented": 22,
+ "not-implemented": 21,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -11663,7 +11663,10 @@
"tests/corpus/3127-spawn-runtime-options.ts",
"tests/corpus/3130-child-generic-streams.ts",
"tests/corpus/3131-child-rest-listeners.js",
- "tests/corpus/3132-child-generic-stream-completion.ts"
+ "tests/corpus/3132-child-generic-stream-completion.ts",
+ "tests/corpus/5627-exec-callback.ts",
+ "tests/fixtures/node-types/child-exec.ts",
+ "tests/harness/project-config.test.ts"
]
},
"anchorSource": "chapter"
@@ -11732,8 +11735,13 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:child_process"
+ "status": "partial",
+ "evidence": "compiler-feature:child_process.child_process.exec",
+ "tests": [
+ "tests/corpus/5627-exec-callback.ts",
+ "tests/fixtures/node-types/child-exec.ts",
+ "tests/harness/project-config.test.ts"
+ ]
},
"anchorSource": "exact"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index e3aec2645..fded9de97 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -322,6 +322,7 @@
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
+ { "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 8cb8c972f..69093a600 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -3033,10 +3033,10 @@ declare module "child_process" {
args?: string[] | null,
options?: ExecSyncOptions,
): unknown;
- /* The async shell form — declared surface without full lowering (the
- * type-level tolerance story): harness code on paths tests don't reach
- * must typecheck; a reached call fences at its site. util.promisify
- * recognizes it statically, as it does execFile. */
+ /* The async shell form. The callback forms lower like execFile's, with
+ * the command line run by /bin/sh -c, and util.promisify recognizes it
+ * statically, as it does execFile; reached calls without a callback
+ * fence at their site. */
export function exec(
command: string,
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts
index b30c65b8d..546c9a209 100644
--- a/packages/compiler/src/backend/llvm/lib-process.ts
+++ b/packages/compiler/src/backend/llvm/lib-process.ts
@@ -19,12 +19,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
const argv = host.emitExpr(e.args[1]!);
const cb = host.emitExpr(callbackArg);
host.moveTemp(cb);
+ const shell = host.emitExpr(e.args[3]!);
const adapter = host.execFileThunkFor(cbT);
host.usesTimers = true;
- host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`);
+ host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
const out = B.tmp();
B.line(
- `${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
+ `${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
);
return host.own({ name: out, type: e.type });
}
diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts
index 037b3e5a7..7bb5f3e62 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -62,8 +62,8 @@ export function lowerBuiltinCall(
loc: SrcLoc,
namespaceMember?: ts.MemberName,
): IrExpr {
- if (bi.module === "child_process" && bi.member === "execFile") {
- return lowerer.lowerExecFileCall(expr, loc);
+ if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
+ return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
}
if (bi.module === "events" && bi.member === "addAbortListener") {
return lowerAddAbortListener(lowerer, expr, loc);
diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process.ts b/packages/compiler/src/frontend/lowering/builtins/child-process.ts
index 8c06a28df..ecec14fc5 100644
--- a/packages/compiler/src/frontend/lowering/builtins/child-process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/child-process.ts
@@ -725,35 +725,57 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
}
/** `execFile(file[, args][, options], callback)` — the asynchronous callback
- * slice. The child starts immediately with stdin/stdout/stderr piped; the
- * runtime captures both outputs and invokes the error-first callback after
- * settlement. The callback may ignore a suffix of `(error, stdout,
+ * slice. With `shell`, `exec(command[, options], callback)`: the command
+ * line runs with /bin/sh -c, as execSync runs it, and failure messages show
+ * the command line as Node's do. The child starts immediately with
+ * stdin/stdout/stderr piped; the runtime captures both outputs and invokes
+ * the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout,
* stderr)`, but every declared parameter must have the Node shape. Inline
* `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter
* is not enforced by the growing native capture, as
* with the synchronous and promisified capture forms. Other options remain
* fenced until their lifecycle can share this asynchronous core. */
-export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
+export function lowerExecFileCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ loc: SrcLoc,
+ shell = false,
+): IrExpr {
+ const what = shell ? "exec" : "execFile";
if (
expr.arguments.some(ts.isSpreadElement) ||
expr.arguments.length < 2 ||
- expr.arguments.length > 4
+ expr.arguments.length > (shell ? 3 : 4)
) {
lowerer.noLowering(
- `execFile with ${expr.arguments.length} arguments`,
+ `${what} with ${expr.arguments.length} arguments`,
expr,
- "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
+ shell
+ ? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
+ : "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
);
}
- const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
- const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
+ const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
- const argv = lowerChildArgsArg(lowerer, argsNode, loc);
- if (expr.arguments.length === 4) {
- const options = expr.arguments[2]!;
+ // exec: /bin/sh -c , execSync's lowering (the runtime reads the
+ // command line back from argv[1] for failure messages); execFile: the
+ // file and its args list.
+ const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first;
+ const argv: IrExpr = shell
+ ? {
+ kind: "arrayLit",
+ elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first],
+ type: arrayOf(STRING),
+ loc,
+ }
+ : lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
+ const optionsNode =
+ expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
+ if (optionsNode) {
+ const options = optionsNode;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
- "execFile with a non-literal options argument",
+ `${what} with a non-literal options argument`,
options,
"pass encoding and maxBuffer in an inline object literal",
);
@@ -774,7 +796,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
const member = optionMember(property);
if (!member)
lowerer.noLowering(
- "execFile with this options shape",
+ `${what} with this options shape`,
property,
"use plain encoding and maxBuffer properties without spreads or computed keys",
);
@@ -784,7 +806,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
(member.value.text !== "utf8" && member.value.text !== "utf-8")
) {
lowerer.noLowering(
- "execFile with a non-literal utf8 encoding",
+ `${what} with a non-literal utf8 encoding`,
member.value,
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
);
@@ -792,14 +814,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
} else if (member.name === "maxBuffer") {
if (!pureNumber(member.value)) {
lowerer.noLowering(
- "execFile with a non-literal maxBuffer",
+ `${what} with a non-literal maxBuffer`,
member.value,
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
);
}
} else {
lowerer.noLowering(
- `execFile option '${member.name}'`,
+ `${what} option '${member.name}'`,
property,
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
);
@@ -815,14 +837,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
- "execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
+ `${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
);
}
if (callback.type.ret.kind !== "void") {
lowerer.unsupported(
"SC1090",
callbackNode,
- "execFile callbacks returning a value (make the callback body a block, or return nothing)",
+ `${what} callbacks returning a value (make the callback body a block, or return nothing)`,
);
}
const errorParam = callback.type.params[0];
@@ -837,7 +859,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
- `execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
+ `${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
);
}
}
@@ -846,9 +868,15 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
lowerer.unsupported(
"SC1090",
callbackNode,
- `execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
+ `${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
);
}
}
- return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
+ return {
+ kind: "libCall",
+ fn: "cp.execFile",
+ args: [cmd, argv, callback, boolLit(shell, loc)],
+ type: CHILD_T,
+ loc,
+ };
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts
index 5947f3714..50a97cead 100644
--- a/packages/compiler/src/frontend/lowering/builtins/errors.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts
@@ -11,6 +11,28 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([
"diff",
]);
+/** True for child_process.ExecException, exec's callback error. Its
+ * `code` is the child's numeric exit status (null after a signal), which
+ * the runtime error's string code slot does not hold, so reads, `in` tests
+ * and typeof tests of `code` refuse instead of answering from the slot. */
+export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
+ const sym = t.getSymbol();
+ return (
+ sym?.name === "ExecException" &&
+ lowerer.checker
+ .declarationsOf(sym)
+ .some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
+ );
+}
+
+export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never {
+ return lowerer.noLowering(
+ "ExecException.code",
+ node,
+ "spawn reports the exit status through its 'close' event",
+ );
+}
+
/** `.code` and `.cause` on an error-hierarchy receiver. The latter reads
* the hidden dyn slot installed by constructor options. The former is NodeJS.ErrnoException's
* member (the fallback declares the same shape): the runtime Error's
@@ -122,6 +144,9 @@ export function lowerErrorCodeProperty(
!(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType())
)
return null;
+ if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) {
+ refuseExecExceptionCode(lowerer, expr);
+ }
const rawReceiver = lowerer.lowerExpr(expr.expression);
const receiver =
rawReceiver.type.kind === "dyn"
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
index c0e178fcd..9c3a8c0cf 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/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts
index 484920a1c..a1626f12d 100644
--- a/packages/compiler/src/frontend/lowering/construction-escapes.ts
+++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts
@@ -124,6 +124,15 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
continue;
}
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
+ // A check that only reads assigned fields and throws or does nothing
+ // with the receiver (`if (this.size < 0) throw new RangeError()`)
+ // does not expose the instance; assignments in its branches are not
+ // definite and still count as exposures.
+ if (
+ (ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
+ !exposes(stmt, depth)
+ )
+ continue;
return false;
}
return true;
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 000000000..7c688dcda
--- /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 56c639397..b3094d5c7 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -59,6 +59,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";
@@ -223,6 +224,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-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 06739166c..7a98096b7 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -143,7 +143,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
import { classPrototypeData } from "./class-prototypes.js";
-import { constructionEscape } from "./construction-escapes.js";
+import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
@@ -468,6 +468,11 @@ export interface ClassInfo {
* unassigned read throws the catchable TypeError instead of yielding
* an undefined the declared type cannot hold (SEMANTICS.md). */
deferredInitFields?: Set;
+ /** Fields (inherited included) that code construction exposes the
+ * instance to can read before their assignment: the slot is the
+ * undefined-armed union, allocation writes undefined, and reads yield the
+ * union value, so an early read sees undefined as in Node. */
+ earlyReadFields?: Set;
/** True when construction may hand the instance to other code (a method
* call, an argument, a callback, or a base constructor that does) — a
* subclass's own fields can then be read before their assignment. */
@@ -3473,55 +3478,54 @@ export function collectClassShapeInner(
const derived =
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
false;
- // Generic instances and mixins share layouts across declarations, so
- // they only report whether construction exposes the instance.
- const escape: { escapes: boolean; assigned: Set | null } =
- familyMode || mixin
- ? { escapes: derived, assigned: null }
- : constructionEscape({
- ctor,
- fields: fieldOrder.flatMap((f) =>
- paramProps.some((pp) => pp.name === f.name)
- ? []
- : [{ name: f.name, initializer: f.initializer }],
- ),
- paramProps: paramProps.map((pp) => pp.name),
- derived,
- baseEscapes:
- base === null ||
- callableBase !== undefined ||
- factoryBaseExpression !== undefined ||
- base.constructionEscapes === true,
- isField: (name) =>
- fields.has(name) &&
- !methods.has(`set:${name}`) &&
- !lowerer.findMethodOn(base, `set:${name}`),
- privateMethodBody: (call) => {
- const callee = call.expression;
- if (!ts.isPropertyAccessExpression(callee)) return null;
- const name = callee.name.text;
- const declaration = decl.members.find(
- (member): member is ts.MethodDeclaration =>
- ts.isMethodDeclaration(member) &&
- member.body !== undefined &&
- (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
- member.name.text === name,
- );
- if (!declaration?.body || declaration.asteriskToken) return null;
- const modifiers = ts.getModifiers(declaration) ?? [];
- if (
- modifiers.some(
- (m) =>
- m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
- )
+ // A generic family is not emitted; its instances and each mixin
+ // application analyze their own construction with their actual base.
+ const escape: { escapes: boolean; assigned: Set | null } = familyMode
+ ? { escapes: derived, assigned: null }
+ : constructionEscape({
+ ctor,
+ fields: fieldOrder.flatMap((f) =>
+ paramProps.some((pp) => pp.name === f.name)
+ ? []
+ : [{ name: f.name, initializer: f.initializer }],
+ ),
+ paramProps: paramProps.map((pp) => pp.name),
+ derived,
+ baseEscapes:
+ base === null ||
+ callableBase !== undefined ||
+ factoryBaseExpression !== undefined ||
+ base.constructionEscapes === true,
+ isField: (name) =>
+ fields.has(name) &&
+ !methods.has(`set:${name}`) &&
+ !lowerer.findMethodOn(base, `set:${name}`),
+ privateMethodBody: (call) => {
+ const callee = call.expression;
+ if (!ts.isPropertyAccessExpression(callee)) return null;
+ const name = callee.name.text;
+ const declaration = decl.members.find(
+ (member): member is ts.MethodDeclaration =>
+ ts.isMethodDeclaration(member) &&
+ member.body !== undefined &&
+ (ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
+ member.name.text === name,
+ );
+ if (!declaration?.body || declaration.asteriskToken) return null;
+ const modifiers = ts.getModifiers(declaration) ?? [];
+ if (
+ modifiers.some(
+ (m) =>
+ m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
)
- return null;
- const isPrivate =
- ts.isPrivateIdentifier(declaration.name) ||
- modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
- return isPrivate ? declaration.body : null;
- },
- });
+ )
+ return null;
+ const isPrivate =
+ ts.isPrivateIdentifier(declaration.name) ||
+ modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
+ return isPrivate ? declaration.body : null;
+ },
+ });
// The deferred definite-assignment check: a field on the unguarded
// list passes only with an unconditional `this.x = ...` at the
// constructor's TOP LEVEL — the same standard the JS-class path
@@ -3577,18 +3581,30 @@ export function collectClassShapeInner(
}
}
- if (escape.assigned && !inst) {
+ // Fields assigned after construction can expose the instance (a method
+ // call, a base constructor that calls an override) can be read while
+ // Node still holds undefined. Their slots admit undefined, allocation
+ // writes it, and reads yield it, as in Node.
+ const earlyReadFields = new Set(base?.earlyReadFields ?? []);
+ if (escape.assigned && escape.escapes) {
for (const f of fieldOrder) {
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
- if (paramProps.some((pp) => pp.name === f.name)) continue;
const declared = fields.get(f.name);
- if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
+ if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
+ // A declared `T | undefined` field already starts undefined.
+ if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
continue;
- const armed = lowerer.withUndefinedArm(declared);
- if (armed === null || armed.kind !== "union") continue;
+ const armed = lowerer.runtimeOptionalType(declared);
+ if (armed.kind !== "union") continue;
fields.set(f.name, armed);
f.type = armed;
- deferredInitFields.add(f.name);
+ // Primitive and union slots yield the undefined value itself.
+ // Reference slots keep the deferred-init reads: consumers that
+ // observe undefined (printing, equality, typeof, nullish defaults)
+ // see it, and member access on it throws a TypeError, as in Node.
+ if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
+ earlyReadFields.add(f.name);
+ else deferredInitFields.add(f.name);
}
}
@@ -3693,6 +3709,43 @@ export function collectClassShapeInner(
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
)
: null;
+ // Constructor statements that run after the instance may have been
+ // exposed (a method call, `this` passed along, a base constructor
+ // that exposes it): a field they create can be read before its
+ // assignment, while Node holds undefined.
+ const exposedBefore = new Set();
+ {
+ let exposed = false;
+ for (const stmt of ctor?.body?.statements ?? []) {
+ const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
+ if (
+ expression &&
+ ts.isCallExpression(expression) &&
+ expression.expression.kind === ts.SyntaxKind.SuperKeyword
+ ) {
+ if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
+ exposed = true;
+ continue;
+ }
+ const assigned =
+ expression &&
+ ts.isBinaryExpression(expression) &&
+ expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isPropertyAccessExpression(expression.left) &&
+ expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
+ ? expression
+ : null;
+ if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
+ const plainAssignment =
+ assigned !== null &&
+ ts.isPropertyAccessExpression(assigned.left) &&
+ !mentionsReceiver(assigned.right) &&
+ !hasAccessor(assigned.left.name.text) &&
+ !methods.has(assigned.left.name.text) &&
+ !lowerer.findMethodOn(base, assigned.left.name.text);
+ if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
+ }
+ }
if (ctor?.body) {
for (const stmt of ctor.body.statements) {
const lhs =
@@ -3763,8 +3816,15 @@ export function collectClassShapeInner(
type = inheritedJsFieldType(lowerer, decl, name, type);
// super() can invoke a subclass setter or method before its
// constructor assignments run. These slots start undefined.
- if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
- type = DYN;
+ const sealed = sealedPrefix?.created.has(name) === true;
+ if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
+ if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
+ const armed = lowerer.runtimeOptionalType(type);
+ if (armed.kind === "union") {
+ type = armed;
+ earlyReadFields.add(name);
+ }
+ }
fields.set(name, type);
fieldOrder.push({ name, type, initializer: undefined });
continue;
@@ -4025,6 +4085,7 @@ export function collectClassShapeInner(
? { classDecorators: { nodes: classDecoratorNodes } }
: {}),
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
+ ...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
...(escape.escapes ? { constructionEscapes: true as const } : {}),
};
// GENERIC members get their declaring-class backlink now that the
@@ -6199,6 +6260,33 @@ function overrideReturnAbi(
return declared;
}
+/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
+const PRIMITIVE_EARLY_READS: ReadonlySet = new Set([
+ "f64",
+ "bool",
+ "string",
+ "bigint",
+]);
+
+/** Field types whose slot can admit undefined for reads before the field's
+ * assignment. Date values are scalars, so their unassigned slot is memory
+ * safe; checked-dynamic slots already hold undefined. */
+function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
+ if (isUnitType(type)) return false;
+ switch (type.kind) {
+ case "dyn":
+ case "void":
+ case "date":
+ case "generator":
+ case "caught":
+ return false;
+ case "object":
+ return lowerer.classes.get(type.className)?.def.runtime !== true;
+ default:
+ return true;
+ }
+}
+
/** The nearest declaration of `name` at or above `info` — the method a
* receiver of that static class runs when nothing below overrides it. */
export function findMethodOn(
@@ -7628,7 +7716,11 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
className: info.def.name,
field: pp.name,
- value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
+ // An early-readable field's slot admits undefined (earlyReadFields).
+ value: lowerer.coerceToExpected(
+ { kind: "varRef", localId: local!.id, type: local!.type, loc },
+ info.fields.get(pp.name) ?? local!.type,
+ ),
loc,
});
}
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index d2d990e16..c8874fe34 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -13,6 +13,7 @@ import { lowerReadlineProperty } from "./builtins/readline.js";
import { lowerBlobInstanceof } from "./builtins/blob.js";
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
+import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
import { threadsImportOf } from "../threads-import.js";
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
@@ -3133,6 +3134,51 @@ export function lowerTernary(
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
* unrepresentable without a re-tag), or to nothing (`never` in an
* exhaustive default) leaves the expression union-typed. */
+/** Uses that read an early-read field's value as Node does when it is
+ * undefined: printing, templates, operators, tests, member access, calls of
+ * program functions (whose parameters the optional-value analysis widens),
+ * and stores into bindings, fields and returns. Other uses, such as library
+ * arguments, extract the declared type with a check. */
+function observesEarlyRead(node: ts.Node): boolean {
+ let child = node;
+ let parent = node.parent;
+ while (parent && ts.isParenthesizedExpression(parent)) {
+ child = parent;
+ parent = parent.parent;
+ }
+ if (!parent) return false;
+ if (
+ ts.isTemplateSpan(parent) ||
+ ts.isBinaryExpression(parent) ||
+ ts.isPrefixUnaryExpression(parent) ||
+ ts.isPostfixUnaryExpression(parent) ||
+ ts.isTypeOfExpression(parent) ||
+ ts.isConditionalExpression(parent) ||
+ ts.isIfStatement(parent) ||
+ ts.isWhileStatement(parent) ||
+ ts.isReturnStatement(parent) ||
+ ts.isVariableDeclaration(parent) ||
+ ts.isPropertyAssignment(parent) ||
+ ts.isArrayLiteralExpression(parent) ||
+ (ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
+ (ts.isElementAccessExpression(parent) && parent.expression === child)
+ )
+ return true;
+ if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
+ const callee = parent.expression;
+ if (
+ ts.isPropertyAccessExpression(callee) &&
+ ts.isIdentifier(callee.expression) &&
+ callee.expression.text === "console"
+ )
+ return true;
+ return (
+ !ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
+ );
+ }
+ return false;
+}
+
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
if (
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
@@ -3343,6 +3389,25 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (expr.type.kind !== "union") return expr;
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
return expr;
+ // A field read before its assignment holds undefined although its
+ // declared type omits it; the declared type is no narrowing proof. A flow
+ // narrowing past the declared type (`if (this.x !== null)`) extracts the
+ // narrowed arm with a check, since the slot may still hold undefined.
+ if (
+ expr.kind === "fieldGet" &&
+ lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
+ ) {
+ const symbol = ts.isPropertyAccessExpression(node)
+ ? lowerer.checker.getSymbolAtLocation(node.name)
+ : undefined;
+ const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
+ const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
+ if (narrowedArm === null || narrowedArm.kind === "union") return expr;
+ if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
+ return expr;
+ const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
+ return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
+ }
// Represented fields and index values can include undefined even when
// checker inference or lib.d.ts declares strings (RegExp captures).
@@ -11326,6 +11391,7 @@ function lowerErrorCodeTypeofTest(
recv = recv.expression;
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
if (!errT) continue;
+ if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
const caught = lowerer.caughtLocalOf(recv);
let receiver: IrExpr;
if (caught) {
@@ -12928,6 +12994,9 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
loc,
};
}
+ if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
+ refuseExecExceptionCode(lowerer, expr);
+ }
let recv = lowerer.lowerExpr(expr.right);
const classInfo =
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
@@ -15103,6 +15172,20 @@ export function fieldGetExpr(
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
}
+ // An early-read field (ClassInfo.earlyReadFields) yields its undefined
+ // where the use reads it as Node does; elsewhere the declared type is
+ // extracted with a check.
+ if (
+ target.fieldType.kind === "union" &&
+ lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
+ !observesEarlyRead(blame)
+ ) {
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
+ if (declared && declared.kind !== "union") {
+ const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
+ if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
+ }
+ }
return read;
}
return {
@@ -15643,6 +15726,22 @@ function lowerFieldCompoundValue(
if (op === "+" && read.type.kind === "string") {
appended = lowerer.ensureString(rhs, rhsNode ?? access);
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
+ } else if (
+ op === "+" &&
+ read.type.kind === "union" &&
+ lowerer.stripUndefinedArm(read.type).kind === "string" &&
+ target.container === "class" &&
+ lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
+ ) {
+ // A string field read before its assignment holds undefined, which
+ // concatenates as "undefined".
+ value = {
+ kind: "strConcat",
+ left: lowerer.ensureString(read, access),
+ right: lowerer.ensureString(rhs, rhsNode ?? access),
+ type: STRING,
+ loc,
+ };
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
} else if (bigintValue) {
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index d18c4f547..81ded6a58 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 cffe4c661..2cc51e789 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);
+ 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] : [];
}
- // 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] : [];
- });
+ // 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 1977ec3a6..fe96156da 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";
@@ -2443,6 +2449,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). */
@@ -2610,6 +2620,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];
@@ -3450,6 +3464,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")
@@ -3933,6 +3948,18 @@ export class Lowerer {
if (!s) return null;
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
};
+ // Fields that can be read before their assignment hold undefined then;
+ // their reads are runtime-optional values like missing array reads.
+ for (const info of this.classes.values()) {
+ if (!info.earlyReadFields || !info.decl?.name) continue;
+ const classSymbol = symbolOf(info.decl.name);
+ if (!classSymbol) continue;
+ const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol);
+ for (const name of info.earlyReadFields) {
+ const property = this.checker.getPropertyOfType(instance, name);
+ if (property) optionalSymbols.add(property);
+ }
+ }
const signatureOf = (node: ts.Identifier): FnSig | null => {
const symbol = symbolOf(node);
if (!symbol) return null;
@@ -6328,6 +6355,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
@@ -11759,8 +11787,8 @@ export class Lowerer {
return lowerSpawnCall(this, expr, loc);
}
- lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
- return lowerExecFileCall(this, expr, loc);
+ lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
+ return lowerExecFileCall(this, expr, loc, shell);
}
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
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 000000000..5375f3792
--- /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 0ecf09e6a..e49b9307e 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.test.ts b/packages/compiler/src/frontend/program.test.ts
index 33abf6d67..8fec2f486 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 3a4ec1647..df0cb673e 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -1312,6 +1312,510 @@ 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"));
+ 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;
+ 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) {
+ 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;
+ }
+ 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;
+ }
+ // A declaration's own name reads nothing.
+ if (named !== undefined && declaredName7(named) === 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)) {
+ // __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))
+ return { kind: "owner", owner: sym };
+ }
+ return null;
+ };
+ const linkTo = (target: Target, node: ts.Node): void => {
+ if (target.kind === "key") {
+ 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]);
+ } 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. */
@@ -2307,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;
@@ -2354,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
@@ -4049,10 +4769,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 +4819,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 +4917,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 +4945,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 +5682,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/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index 52964e1bd..8157d8c04 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -1386,6 +1386,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "object", className: "%Error" };
}
+ // child_process.ExecException, exec's callback error, is an interface
+ // extending Error with optional metadata members; the value is one
+ // ordinary Error as well.
+ const execException = type.getSymbol();
+ if (
+ execException?.name === "ExecException" &&
+ checker
+ .declarationsOf(execException)
+ .some(
+ (decl) =>
+ ts.isInterfaceDeclaration(decl) &&
+ ctx.isStdlibFile(decl.getSourceFile()) &&
+ isDeclaredInAmbientModule(decl, "child_process"),
+ )
+ ) {
+ return { kind: "object", className: "%Error" };
+ }
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
// Immutable (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 7f31c5205..ca2749591 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -2343,8 +2343,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** child_process.execFile's callback slice: starts an all-piped child,
* captures stdout/stderr, and moves the error-first callback into the
* child registry. The callback shape is program-dependent and checked
- * by the validator/backends. */
- "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
+ * by the validator/backends. The last argument is exec's shell flag:
+ * the argv is ["-c", command] and failure messages show the command. */
+ "cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T },
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
// 2 fd) with the out/err fds for mode 2, detached, env replacement
// pairs, cwd ("" = inherit).
diff --git a/packages/compiler/src/ir/validate.test.ts b/packages/compiler/src/ir/validate.test.ts
index d44a187f4..b7caf8c41 100644
--- a/packages/compiler/src/ir/validate.test.ts
+++ b/packages/compiler/src/ir/validate.test.ts
@@ -951,6 +951,7 @@ test("library callbacks retain child, specialized, and generic result diagnostic
{ kind: "strLit", value: "tool", type: STRING, loc },
{ kind: "arrayLit", elems: [], type: arrayOf(STRING), loc },
{ kind: "boolLit", value: true, type: F64, loc },
+ { kind: "boolLit", value: false, type: BOOL, loc },
],
};
expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([
diff --git a/packages/compiler/test/class-early-field-reads.test.ts b/packages/compiler/test/class-early-field-reads.test.ts
new file mode 100644
index 000000000..c602a2f42
--- /dev/null
+++ b/packages/compiler/test/class-early-field-reads.test.ts
@@ -0,0 +1,93 @@
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { expect, test } from "vitest";
+import { compile, deserializeModule, validateModule } from "../src/index.js";
+import type { IrModule } from "../src/ir/ir.js";
+
+async function lower(source: string, file = "main.ts"): Promise {
+ const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-"));
+ try {
+ const entry = join(dir, file),
+ output = join(dir, "main.ir.json");
+ await writeFile(entry, source);
+ const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
+ if (!result.ok)
+ throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
+ const module = deserializeModule(await readFile(output, "utf8"));
+ expect(validateModule(module)).toEqual([]);
+ return module;
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ }
+}
+
+/** Field name → IR kind for a class. */
+function fields(module: IrModule, name: string): string {
+ const cls = module.classes.find((candidate) => candidate.name === name);
+ expect(cls, name).toBeDefined();
+ return cls!.fields
+ .filter((field) => !field.name.startsWith("%"))
+ .map((field) => `${field.name}:${field.type.kind}`)
+ .join(" ");
+}
+
+test("fields assigned after construction can call an override admit undefined", async () => {
+ const module = await lower(`
+abstract class Base {
+ constructor() {
+ this.describe();
+ }
+ abstract describe(): void;
+}
+class Panel extends Base {
+ label: string;
+ width: number;
+ count = 0;
+ constructor() {
+ super();
+ this.label = "panel";
+ this.width = 2;
+ }
+ describe(): void {
+ console.log(this.label, this.width, this.count);
+ }
+}
+class Plain {
+ label: string;
+ width: number;
+ constructor() {
+ this.label = "plain";
+ this.width = 1;
+ this.describe();
+ }
+ describe(): void {
+ console.log(this.label, this.width);
+ }
+}
+new Panel();
+new Plain();
+`);
+ expect(fields(module, "Panel")).toBe("label:union width:union count:union");
+ expect(fields(module, "Plain")).toBe("label:string width:f64");
+});
+
+test("JavaScript fields created after the constructor calls a method admit undefined", async () => {
+ const module = await lower(
+ `
+class Widget {
+ constructor() {
+ this.ready = true;
+ this.render();
+ this.title = "widget";
+ }
+ render() {
+ console.log(this.ready, this.title);
+ }
+}
+new Widget();
+`,
+ "main.js",
+ );
+ expect(fields(module, "Widget")).toBe("ready:bool title:union");
+});
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json
new file mode 100644
index 000000000..cbefe8acc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5627-exec-callback.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5627-exec-callback.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json
new file mode 100644
index 000000000..d2dcdcc24
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5943-fields-read-before-assignment.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5943-fields-read-before-assignment.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json
new file mode 100644
index 000000000..e9800b71d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5944-js-fields-read-before-assignment.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5944-js-fields-read-before-assignment.js"
+ ],
+ "diags": []
+}
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 000000000..db768b31b
--- /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 000000000..b074bf18d
--- /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 000000000..55720ef1a
--- /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 000000000..92fe169c3
--- /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 000000000..df78ca9ed
--- /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 000000000..3e8122d1f
--- /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 a54923bcf..67d01f47e 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 000000000..aaeea4cf2
--- /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 d15d404ae..205a8cd6e 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/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json
new file mode 100644
index 000000000..8f8a2a4b8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec-code-fences.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/child-exec-code-fences.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json
new file mode 100644
index 000000000..73db22aca
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/child-exec.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/node-types/child-exec.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts
index 660394a6f..e7479c749 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);
diff --git a/packages/runtime/src/scr_child.c b/packages/runtime/src/scr_child.c
index 2d72c21d2..1235001ba 100644
--- a/packages/runtime/src/scr_child.c
+++ b/packages/runtime/src/scr_child.c
@@ -2266,14 +2266,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
- ScrExecFileFn fn) {
+ ScrExecFileFn fn, bool shell) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
- char *display = scr_exec_display(cmd, args, false);
+ char *display = scr_exec_display(cmd, args, shell);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
@@ -4911,14 +4911,14 @@ static void scr_exec_file_fire(ScrChild *c) {
}
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
- ScrExecFileFn fn) {
+ ScrExecFileFn fn, bool shell) {
ScrStr *empty = scr_str_new("", 0);
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
NULL, empty);
scr_str_release(empty);
c->exec_cb = cb;
c->exec_fn = fn;
- char *display = scr_exec_display(cmd, args, false);
+ char *display = scr_exec_display(cmd, args, shell);
c->exec_display = scr_str_new(display, strlen(display));
free(display);
scr_child_stream_capture_begin(c->out_stream);
diff --git a/packages/runtime/src/scr_child.h b/packages/runtime/src/scr_child.h
index 350995e31..1819b9c53 100644
--- a/packages/runtime/src/scr_child.h
+++ b/packages/runtime/src/scr_child.h
@@ -155,9 +155,11 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
/* The callback slice: file + args, default options. The returned child has
* all three stdio slots piped; stdout/stderr are captured internally and
- * the callback moves into the child registry until settlement. */
+ * the callback moves into the child registry until settlement. With shell
+ * (exec), args is ["-c", command] for /bin/sh and failure messages show
+ * the command, as for scr_exec_sync. */
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
- ScrExecFileFn fn);
+ ScrExecFileFn fn, bool shell);
ScrChild *scr_child_retain(ScrChild *c);
void scr_child_release(ScrChild *c);
void *scr_child_retain_v(void *p);
diff --git a/tests/corpus/5627-exec-callback.ts b/tests/corpus/5627-exec-callback.ts
new file mode 100644
index 000000000..eed79e738
--- /dev/null
+++ b/tests/corpus/5627-exec-callback.ts
@@ -0,0 +1,36 @@
+// child_process.exec's callback forms: the command line runs with
+// /bin/sh -c, the call returns a live ChildProcess immediately, utf8
+// stdout/stderr are captured, and the error-first callback fires after the
+// current turn. A non-zero exit (a missing command is the shell's 127)
+// reports "Command failed: \n".
+import { exec } from "node:child_process";
+import * as childProcess from "node:child_process";
+
+const child = exec("printf out; printf err >&2", (error, stdout, stderr) => {
+ console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr));
+ exec("printf 'a\\nb\\nc\\n' | wc -l", (pipeError, lines) => {
+ console.log("pipe", pipeError === null, lines.trim());
+ exec(`printf '%s|' "a b" 'c d' e`, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (_, quoted) => {
+ console.log("quoting", quoted);
+ exec("printf partial; printf failed >&2; exit 3", (failed, failedOut, failedErr) => {
+ console.log("fail", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr));
+ console.log("fail-message", JSON.stringify(failed === null ? "" : failed.message));
+ exec("exit 1", (silent) => {
+ console.log("silent", JSON.stringify(silent === null ? "" : silent.message));
+ exec("scriptc-definitely-not-a-command", (missing, missingOut) => {
+ const message = missing === null ? "" : missing.message;
+ console.log("missing", missing !== null, JSON.stringify(missingOut));
+ console.log("missing-message", message.split("\n")[0]);
+ exec("true", () => {
+ console.log("zero-parameter callback");
+ childProcess.exec("printf namespace", (_, out) => console.log(out));
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+console.log("returned", child.pid !== undefined, child.stdout !== null, child.stderr !== null);
+console.log("scheduled");
diff --git a/tests/corpus/5943-fields-read-before-assignment.ts b/tests/corpus/5943-fields-read-before-assignment.ts
new file mode 100644
index 000000000..922b3c34d
--- /dev/null
+++ b/tests/corpus/5943-fields-read-before-assignment.ts
@@ -0,0 +1,181 @@
+// @transform-types
+// Fields that a base constructor (or the constructor itself) can read
+// through an override or a method call before they are assigned hold
+// undefined then, as in Node, for every field kind. After construction
+// the fields read and update normally.
+class Point {
+ x: number;
+ constructor(x: number) {
+ this.x = x;
+ }
+}
+function show(label: string, value: number): string {
+ return `${label}=${value}`;
+}
+abstract class Widget {
+ log: string[] = [];
+ constructor() {
+ this.describe("base");
+ }
+ abstract describe(when: string): void;
+}
+class Panel extends Widget {
+ label: string;
+ width: number;
+ visible: boolean;
+ origin: Point;
+ items: number[];
+ options: { depth: number };
+ key: string | number;
+ index: Map;
+ tags: Set;
+ count = 5;
+ constructor(label: string, readonly id: string) {
+ super();
+ this.label = label;
+ this.width = 10;
+ this.visible = true;
+ this.origin = new Point(1);
+ this.items = [1, 2];
+ this.options = { depth: 2 };
+ this.key = 7;
+ this.index = new Map([["a", 1]]);
+ this.tags = new Set(["t"]);
+ this.describe("ctor");
+ }
+ describe(when: string): void {
+ console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id);
+ console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t"));
+ console.log(when, show(this.label, this.width), String(this.key), [this.label].length);
+ try {
+ console.log(this.label.length, this.items.length);
+ } catch (error) {
+ console.log(when, (error as Error).message);
+ }
+ }
+ grow(): number {
+ this.count++;
+ this.width += 5;
+ this.label += "!";
+ this.items.push(this.width);
+ return this.count + this.width + this.items.length;
+ }
+}
+const panel = new Panel("main", "p1");
+console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width);
+
+class Plain {
+ total: number;
+ name: string;
+ constructor() {
+ this.total = 1;
+ this.name = "plain";
+ this.bump();
+ }
+ bump(): void {
+ this.total++;
+ }
+}
+const plain = new Plain();
+console.log(plain.total, plain.name);
+
+class Toucher {
+ constructor() {
+ this.touch();
+ }
+ touch(): void {}
+}
+class Counter extends Toucher {
+ count = 0;
+ label: string;
+ items: string[];
+ constructor() {
+ super();
+ this.label = "c";
+ this.items = [];
+ }
+ override touch(): void {
+ this.count++;
+ this.label += "x";
+ console.log(this.count, this.label, Number.isNaN(this.count));
+ try {
+ this.items.push("a");
+ } catch (error) {
+ console.log(error instanceof TypeError);
+ }
+ }
+}
+const c = new Counter();
+c.touch();
+console.log(c.count, c.label, c.items);
+
+class Shower {
+ constructor() {
+ this.show();
+ }
+ show(): void {}
+}
+class Box extends Shower {
+ label: string;
+ value: T;
+ constructor(value: T) {
+ super();
+ this.label = "box";
+ this.value = value;
+ }
+ override show(): void {
+ console.log("show", this.label, this.value);
+ }
+}
+new Box(3).show();
+new Box("s").show();
+
+type Ctor = new (...args: any[]) => T;
+class Viewer {
+ constructor() {
+ this.show();
+ }
+ show(): void {}
+}
+function Labeled>(Parent: B) {
+ return class extends Parent {
+ label = "mixed";
+ override show(): void {
+ console.log("show", this.label);
+ }
+ };
+}
+const L = Labeled(Viewer);
+new L().show();
+
+interface Crash { loc: number; message: string }
+class Setupper {
+ constructor() {
+ this.setup();
+ }
+ setup(): void {}
+}
+class Host extends Setupper {
+ crash: Crash | null;
+ name: string | null;
+ constructor(c: Crash | null) {
+ super();
+ this.crash = c;
+ this.name = "host";
+ }
+ override setup(): void {
+ console.log("setup", this.crash, this.name);
+ if (this.crash !== null) {
+ try {
+ console.log(this.crash.loc);
+ } catch (error) {
+ console.log(error instanceof TypeError);
+ }
+ }
+ }
+ report(): string {
+ if (this.crash) return `${this.crash.message} at ${this.crash.loc}`;
+ return this.name === null ? "none" : this.name.toUpperCase();
+ }
+}
+console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report());
diff --git a/tests/corpus/5944-js-fields-read-before-assignment.js b/tests/corpus/5944-js-fields-read-before-assignment.js
new file mode 100644
index 000000000..e313a12b0
--- /dev/null
+++ b/tests/corpus/5944-js-fields-read-before-assignment.js
@@ -0,0 +1,28 @@
+// JavaScript class fields that the constructor creates after it calls a
+// method, or after a base constructor that does, hold undefined until
+// they are assigned, as in Node.
+class Widget {
+ constructor(name) {
+ this.render();
+ this.name = name;
+ this.size = 3;
+ this.title = "widget";
+ this.parts = ["a"];
+ this.meta = { id: 1 };
+ }
+ render() {
+ console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`);
+ }
+}
+class Big extends Widget {
+ constructor() {
+ super("big");
+ this.extra = 2;
+ }
+ render() {
+ super.render();
+ console.log("big", this.extra);
+ }
+}
+new Widget("w").render();
+new Big().render();
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 000000000..2a9669e12
--- /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 000000000..13163137e
--- /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 000000000..a3c15a7a6
--- /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 000000000..24ef13e5d
--- /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 000000000..aa15c97d6
--- /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 000000000..917eb7874
--- /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 000000000..a3c15a7a6
--- /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/bundle.js b/tests/corpus/6142-cjs-require-published-early/bundle.js
new file mode 100644
index 000000000..c12387219
--- /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/comparator.js b/tests/corpus/6142-cjs-require-published-early/comparator.js
new file mode 100644
index 000000000..2b2ea6146
--- /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/core.js b/tests/corpus/6142-cjs-require-published-early/core.js
new file mode 100644
index 000000000..9e3a9a565
--- /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/debug.js b/tests/corpus/6142-cjs-require-published-early/debug.js
new file mode 100644
index 000000000..30451569b
--- /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 000000000..3d2907f00
--- /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 000000000..206ca7785
--- /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 000000000..310dacdb0
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/main.js
@@ -0,0 +1,15 @@
+// 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 { 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/6142-cjs-require-published-early/package.json b/tests/corpus/6142-cjs-require-published-early/package.json
new file mode 100644
index 000000000..a3c15a7a6
--- /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 000000000..0872049ea
--- /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 000000000..b794d1d4c
--- /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 000000000..b1cf3d3bc
--- /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 000000000..98ace19c0
--- /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 000000000..b67c39f3e
--- /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 000000000..a3c15a7a6
--- /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 000000000..305876762
--- /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 000000000..a3c15a7a6
--- /dev/null
+++ b/tests/corpus/6144-cjs-require-builtin-nested/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
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 000000000..1f0d4eb9d
--- /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 000000000..90cbc8824
--- /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 000000000..a3c15a7a6
--- /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 000000000..ab71e4a52
--- /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 000000000..4e13760cd
--- /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 619117b91..376aa0a36 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/diagnostics/cjs-require-tdz-unchecked/lib.js b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js
new file mode 100644
index 000000000..2a9669e12
--- /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 000000000..52b12fcb4
--- /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 000000000..5bbefffba
--- /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 a571b4524..c2b5a945c 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/fixtures/node-types/child-exec-code-fences.ts b/tests/fixtures/node-types/child-exec-code-fences.ts
new file mode 100644
index 000000000..71f627df4
--- /dev/null
+++ b/tests/fixtures/node-types/child-exec-code-fences.ts
@@ -0,0 +1,8 @@
+import { exec } from "node:child_process";
+
+exec("exit 3", (error) => {
+ if (error === null) return;
+ console.log(error.code);
+ console.log("code" in error);
+ console.log(typeof error.code === "number");
+});
diff --git a/tests/fixtures/node-types/child-exec.ts b/tests/fixtures/node-types/child-exec.ts
new file mode 100644
index 000000000..97e9c546d
--- /dev/null
+++ b/tests/fixtures/node-types/child-exec.ts
@@ -0,0 +1,9 @@
+import { exec } from "node:child_process";
+
+exec("printf typed-exec; printf err >&2", (error, stdout, stderr) => {
+ if (error !== null) throw error;
+ console.log(stdout, stderr);
+ exec("exit 2", { encoding: "utf8" }, (failed) => {
+ console.log(JSON.stringify(failed === null ? "" : failed.message));
+ });
+});
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 000000000..2b9a88f63
--- /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 000000000..a7ae8ee9b
--- /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 000000000..501287cd3
--- /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 000000000..b6d0b7d67
--- /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 000000000..5bd884c25
--- /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 000000000..b09c7c7e6
--- /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 000000000..b40305e73
--- /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 000000000..3edc57d21
--- /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 000000000..5931fd23e
--- /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 000000000..e68df7bde
--- /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 000000000..644cd814b
--- /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 000000000..28a316182
--- /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 000000000..1333ed10b
--- /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 000000000..ea351c553
--- /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 000000000..8c3e719b5
--- /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 ad55be524..8916284c3 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/__snapshots__/cjs-require-tdz-unchecked/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt
new file mode 100644
index 000000000..eda869e9a
--- /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 6316ce98b..295f7cd0c 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
diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json
index da4cd097f..cccd1bd8c 100644
--- a/tests/harness/corpus-costs.json
+++ b/tests/harness/corpus-costs.json
@@ -1834,6 +1834,7 @@
"5566-fs-dirent-dyn-boundaries/main.mjs": 2607,
"5585-record-results-inlined.ts": 2922,
"5595-net-ref-unref.ts": 2593,
+ "5627-exec-callback.ts": 1900,
"5644-net-socket-addresses.ts": 2220,
"5645-net-socket-callbacks.ts": 2442,
"5646-net-server-connections.ts": 2389,
@@ -1901,6 +1902,8 @@
"5923-matchall-groups-js.js": 3970,
"5939-js-let-object-reassigned-wider.js": 1945,
"5942-local-class-observed-methods.ts": 5063,
+ "5943-fields-read-before-assignment.ts": 6856,
+ "5944-js-fields-read-before-assignment.js": 4247,
"5946-nested-record-field-optional-writes.ts": 6045,
"5950-tuple-predicate-methods.ts": 5990,
"5951-result-never-widening.ts": 4710,
@@ -4535,6 +4538,7 @@
"5566-fs-dirent-dyn-boundaries/main.mjs": 3333,
"5585-record-results-inlined.ts": 3681,
"5595-net-ref-unref.ts": 3036,
+ "5627-exec-callback.ts": 2500,
"5644-net-socket-addresses.ts": 2393,
"5645-net-socket-callbacks.ts": 2060,
"5646-net-server-connections.ts": 2755,
@@ -4602,6 +4606,8 @@
"5923-matchall-groups-js.js": 6650,
"5939-js-let-object-reassigned-wider.js": 4436,
"5942-local-class-observed-methods.ts": 6212,
+ "5943-fields-read-before-assignment.ts": 72293,
+ "5944-js-fields-read-before-assignment.js": 40310,
"5946-nested-record-field-optional-writes.ts": 68117,
"5950-tuple-predicate-methods.ts": 7790,
"5951-result-never-widening.ts": 6120,
diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts
index 89bd2e0b8..c5021fd06 100644
--- a/tests/harness/npm-static.test.ts
+++ b/tests/harness/npm-static.test.ts
@@ -912,6 +912,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"), {
diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts
index bbd6045e5..f5e80beb8 100644
--- a/tests/harness/project-config.test.ts
+++ b/tests/harness/project-config.test.ts
@@ -462,6 +462,40 @@ test("node-types: unsupported child stdin forms remain named fences", async () =
]);
});
+test("node-types: callback exec uses the typed ExecException overloads", async () => {
+ const dir = outDirFor("node-child-exec");
+ const entry = join(nodeTypesDir, "child-exec.ts");
+ const result = await compile(entry, {
+ outPath: join(dir, "child-exec"),
+ outDir: dir,
+ sanitize,
+ });
+ expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
+ if (!result.ok) return;
+ const { stdout } = await execFileAsync(result.binaryPath);
+ expect(stdout).toBe('typed-exec err\n"Command failed: exit 2\\n"\n');
+});
+
+test("node-types: exec's numeric ExecException.code remains a named fence", async () => {
+ const dir = outDirFor("node-child-exec-code-fences");
+ const entry = join(nodeTypesDir, "child-exec-code-fences.ts");
+ const result = await compile(entry, {
+ outPath: join(dir, "child-exec-code-fences"),
+ outDir: dir,
+ sanitize,
+ });
+ expect(result.ok).toBe(false);
+ if (result.ok) return;
+ expect(result.diagnostics.map((diagnostic) => diagnostic.code)).toEqual([
+ "SC2020",
+ "SC2020",
+ "SC2020",
+ ]);
+ for (const diagnostic of result.diagnostics) {
+ expect(diagnostic.message).toContain("ExecException.code");
+ }
+});
+
test("node-types: path and os lower statically under @types/node's shapes", async () => {
const outDir = outDirFor("node-path-os");
const result = await compile(join(nodeTypesDir, "path-os.ts"), {
diff --git a/tests/harness/windows-differential.test.ts b/tests/harness/windows-differential.test.ts
index 7e6ed205d..dc199d38f 100644
--- a/tests/harness/windows-differential.test.ts
+++ b/tests/harness/windows-differential.test.ts
@@ -149,6 +149,8 @@ const WINDOWS_SKIPS: Record = {
"posix-shaped: the uncaught sh ENOENT throw crashes both sides, rendered differently (1645 covers env here)",
"1464-env-writes.ts":
'posix-shaped: the observing child is ENOENT on Windows, exposing the documented spawn-failure ""-vs-null stance',
+ "5627-exec-callback.ts":
+ "posix-shaped: exec runs /bin/sh -c (execSync's static shell lowering); Windows Node runs cmd.exe",
// Two programs the events unit's win32 arm surfaced (they compiled for
// the first time once the events gate lifted): both drive their signal
// listeners through a /bin/sh child, ENOENT on Windows Node too.