diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index f939522217..5ecc144c60 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -69,6 +69,10 @@ $ scriptc build app.ts --npm-static=three -o app >/dev/null
Naming a package skips the declaration requirement above, but the package still has to compile. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+## CommonJS packages
+
+CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
+
## Unsupported code inside a package
A JavaScript package can contain operations scriptc can't compile, often in functions your program never calls. These don't fail the build: each one compiles to code that throws a runtime error if it ever runs. Coverage reports list them as deferred sites, so review the list and test the parts of the package your program uses.
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index ed2c5b1ec3..544bd2b2dd 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -32,6 +32,8 @@ title: "Limitations"
../limitations/language-features/dynamic-imports.mdx
+../limitations/language-features/commonjs-require.mdx
+
### Types and class definitions
../limitations/types-and-class-definitions/bigint.mdx
diff --git a/docs/content/limitations/language-features/commonjs-require.mdx b/docs/content/limitations/language-features/commonjs-require.mdx
new file mode 100644
index 0000000000..5784994103
--- /dev/null
+++ b/docs/content/limitations/language-features/commonjs-require.mdx
@@ -0,0 +1 @@
+- **CommonJS `require`:** Literal `require()` calls of compiled modules, supported builtins, and JSON documents compile. Reading a top-level `const` or `let` require binding before its `require` runs throws the same `ReferenceError` as Node.js. A `var` binding that code above its `require` can reach is unsupported, and so is a read that compiles to no checkable expression, such as exporting the binding before the `require`. JSON documents are parsed at build time, and every `require()` of a document returns the same object. Destructuring the exports of a module supports plain and renamed properties, plus nested patterns for builtin sub-modules such as `util.types`. Any pattern works on a JSON document. Defaults, rest elements, and nested patterns over other modules are unsupported. A require cycle compiles when each of its modules assigns all of its exports before its first `require` and uses the other modules of the cycle only inside functions that its top-level code can't call. Other require cycles are rejected with `SC1016`.
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
index c0e178fcde..9c3a8c0cfc 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
@@ -34,21 +34,48 @@ export function builtinImportOf(
// binding (`const { readFileSync } = require("fs")`, renames via
// `{ readFileSync: rf }`) keys the same tables.
if (ts.isBindingElement(decl) && decl.name !== undefined && ts.isIdentifier(decl.name)) {
- const varDecl = decl.parent?.parent;
+ // A NESTED pattern names a member of a sub-module (`const { types: {
+ // isPromise } } = require("util")`): util.types IS the util/types
+ // module object, so each level composes the module name.
+ const path: string[] = [];
+ let pattern: ts.Node | undefined = decl.parent;
+ let varDecl = pattern?.parent;
+ while (
+ pattern !== undefined &&
+ ts.isObjectBindingPattern(pattern) &&
+ varDecl !== undefined &&
+ ts.isBindingElement(varDecl)
+ ) {
+ const key = varDecl.propertyName;
+ if (varDecl.initializer !== undefined || key === undefined || !ts.isIdentifier(key)) {
+ return null;
+ }
+ path.unshift(key.text);
+ pattern = varDecl.parent;
+ varDecl = pattern?.parent;
+ }
if (
- ts.isObjectBindingPattern(decl.parent) &&
+ pattern !== undefined &&
+ ts.isObjectBindingPattern(pattern) &&
+ varDecl !== undefined &&
ts.isVariableDeclaration(varDecl) &&
varDecl.initializer !== undefined
) {
const spec = requireSpecOf(varDecl.initializer);
- const module =
+ let module =
spec !== null
? canonicalBuiltinModule(spec)
- : // The one-hop alias twin: `const crypto = require('crypto');
- // const { createSign } = crypto;` — test/common's idiom. The
- // destructure re-binds module members under local names, so the
- // bindings key the same tables as the direct-require form.
- builtinNamespaceDestructureModuleOf(lowerer, varDecl);
+ : path.length > 0
+ ? null
+ : // The one-hop alias twin: `const crypto = require('crypto');
+ // const { createSign } = crypto;` — test/common's idiom. The
+ // destructure re-binds module members under local names, so the
+ // bindings key the same tables as the direct-require form.
+ builtinNamespaceDestructureModuleOf(lowerer, varDecl);
+ for (const key of path) {
+ if (module === null) break;
+ module = canonicalBuiltinModule(`${module}/${key}`);
+ }
if (module === null) return null;
const member =
decl.propertyName && ts.isIdentifier(decl.propertyName)
diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts
new file mode 100644
index 0000000000..7c688dcda0
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/json-require.ts
@@ -0,0 +1,140 @@
+/* CommonJS require() of JSON modules. Node parses the document at the
+ * first require and caches the resulting object: every later require, from
+ * any module, answers that same object, so a mutation through one requirer
+ * is visible to all of them. The document is data known at build time, so
+ * the object bakes from the parsed text (comptimeValueToIr, as JSON import
+ * declarations do) into one global per document. The parse has no
+ * observable effect, so each requiring module creates the object in its
+ * %init prelude unless an earlier module already did, and every require
+ * call of the document reads the shared global. A require binding of a
+ * JSON module is ordinary storage holding that object — destructuring
+ * copies its properties at the require, exactly like Node. ES-module JSON
+ * imports keep their own record (Node's ESM loader caches separately). */
+import * as ts from "../ts7/adapter.js";
+import type { Lowerer } from "./lowerer.js";
+import { PoisonError } from "./lowerer.js";
+import type { FileParts } from "./lower-modules.js";
+import { isCjsJsFile, locOf, requireSpecOf, resolveImport } from "../program.js";
+import { createRequireCalleeFileOf } from "./builtins/module-bindings.js";
+import { invalidJsonModuleDiag } from "../../diagnostics/diagnostic.js";
+import { BOOL, type IrExpr, type IrGlobal, type IrType } from "../../ir/ir.js";
+
+/** One required JSON document: its shared global, the run-once flag, and
+ * the parsed value with the type it bakes to. */
+export interface JsonRequireModule {
+ global: IrGlobal;
+ loadedId: string;
+ parsed: unknown;
+ type: IrType;
+}
+
+/** The JSON document a CommonJS `require("…")` call loads, or null. */
+export function jsonRequireTarget(
+ lowerer: Lowerer,
+ call: ts.Expression | undefined,
+): ts.SourceFile | null {
+ if (call === undefined || !ts.isCallExpression(call)) return null;
+ const spec = requireSpecOf(call);
+ if (spec === null || !ts.isIdentifier(call.expression)) return null;
+ const sf = call.getSourceFile();
+ if (!isCjsJsFile(sf, lowerer.program)) return null;
+ if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
+ const target = resolveImport(lowerer.program, sf, spec, "require");
+ return target !== null && target.fileName.endsWith(".json") ? target : null;
+}
+
+/** A declaration whose initializer is a require() of a JSON document. */
+export function isJsonRequireDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
+ return jsonRequireTarget(lowerer, decl.initializer) !== null;
+}
+
+/** Registers the shared global of every JSON document a CommonJS module of
+ * the program requires, and the guarded creation in each requiring
+ * module's %init prelude. */
+export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void {
+ for (const fp of parts) {
+ const sf = fp.sf;
+ if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue;
+ if (!sf.text.includes(".json")) continue;
+ const calls: ts.CallExpression[] = [];
+ ts.walkPreorder(sf, (node) => {
+ if (ts.isCallExpression(node) && requireSpecOf(node) !== null) calls.push(node);
+ return undefined;
+ });
+ const created = new Set();
+ for (const call of calls) {
+ const jsonSf = jsonRequireTarget(lowerer, call);
+ if (jsonSf === null) continue;
+ try {
+ let mod = lowerer.jsonRequireModules.get(jsonSf.fileName);
+ if (mod === undefined) {
+ const tsType = lowerer.typeOf(call);
+ const mapped = lowerer.mapTypeOf(tsType);
+ if (!mapped || !lowerer.comptimeBakeable(mapped)) lowerer.badType(call, tsType);
+ let parsed: unknown;
+ try {
+ parsed = JSON.parse(jsonSf.text);
+ } catch (e) {
+ lowerer.pushDiag(
+ invalidJsonModuleDiag(
+ jsonSf.fileName,
+ e instanceof Error ? e.message : String(e),
+ locOf(call),
+ ),
+ );
+ throw new PoisonError();
+ }
+ const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`;
+ const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true };
+ const loadedId = `${id}%loaded`;
+ lowerer.globalsList.push(global, {
+ id: loadedId,
+ name: "%loaded",
+ type: BOOL,
+ mutable: true,
+ });
+ mod = { global, loadedId, parsed, type: mapped };
+ lowerer.jsonRequireModules.set(jsonSf.fileName, mod);
+ }
+ lowerer.jsonRequireCalls.set(call, mod);
+ if (created.has(mod)) continue;
+ created.add(mod);
+ const loc = locOf(call);
+ const value = lowerer.comptimeValueToIr(mod.parsed, mod.type, "$", call);
+ const actions = lowerer.jsonInitActions.get(sf) ?? [];
+ lowerer.jsonInitActions.set(sf, actions);
+ actions.push({
+ kind: "if",
+ cond: {
+ kind: "unary",
+ op: "!",
+ operand: { kind: "varRef", localId: mod.loadedId, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ },
+ then: [
+ {
+ kind: "assign",
+ localId: mod.loadedId,
+ value: { kind: "boolLit", value: true, type: BOOL, loc },
+ loc,
+ },
+ { kind: "assign", localId: mod.global.id, value, loc },
+ ],
+ else_: null,
+ loc,
+ });
+ } catch (e) {
+ if (!(e instanceof PoisonError)) throw e;
+ // diagnostic already recorded
+ }
+ }
+ }
+}
+
+/** A require() call of a JSON document: the shared object. */
+export function lowerJsonRequireCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const mod = lowerer.jsonRequireCalls.get(call);
+ if (mod === undefined) return null;
+ return { kind: "varRef", localId: mod.global.id, type: mod.global.type, loc: locOf(call) };
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index 56c639397b..b3094d5c7c 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-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index d18c4f547b..81ded6a585 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -35,6 +35,8 @@ import {
withClassInitFlag,
withCycleInitFlags,
} from "./cycle-initialization.js";
+import { markRequireEarlyBindings } from "./require-initialization.js";
+import { collectJsonRequires, isJsonRequireDecl } from "./json-require.js";
import {
invalidJsonModuleDiag,
unsupportedPackageImportDiag,
@@ -409,6 +411,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts);
lowerer.collectNpmImports(parts);
lowerer.collectJsonImports(parts);
+ collectJsonRequires(lowerer, parts);
} finally {
lowerer.diagSink = null;
}
@@ -416,6 +419,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
for (const fp of parts) lowerer.collectGlobals(fp.sf, fp.topStmts);
lowerer.collectNpmImports(parts);
lowerer.collectJsonImports(parts);
+ collectJsonRequires(lowerer, parts);
}
for (const decl of computed) {
const symbol = declSymbolOf(lowerer, decl);
@@ -493,6 +497,7 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
lowerer.collecting = wasCollecting;
}
markCycleEarlyBindings(lowerer);
+ markRequireEarlyBindings(lowerer);
}
export function collectNpmImports(lowerer: Lowerer, parts: FileParts[]): void {
@@ -1532,8 +1537,9 @@ export function collectGlobals(
isRequireStatement(stmt) &&
!(
ts.isVariableStatement(stmt) &&
- stmt.declarationList.declarations.some((decl) =>
- isNativeFfiRequire(lowerer, decl.initializer),
+ stmt.declarationList.declarations.some(
+ (decl) =>
+ isNativeFfiRequire(lowerer, decl.initializer) || isJsonRequireDecl(lowerer, decl),
)
)
) {
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index cffe4c6619..2cc51e7898 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -10,6 +10,8 @@ import { InternalCompilerError } from "../../errors.js";
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
* explicit resource management, and blocked-binding poisoning. */
import * as ts from "../ts7/adapter.js";
+import { requireInitFlagStmt } from "./require-initialization.js";
+import { isJsonRequireDecl } from "./json-require.js";
import { bindingSource } from "../binding-source.js";
import type { Lowerer } from "./lowerer.js";
import {
@@ -1512,81 +1514,96 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement):
const isLet =
(list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0;
return list.declarations.flatMap((decl) => {
- // The canonical ESM bridge to a compiled program module. Like a
- // top-level CommonJS require declaration, the binding is alias
- // plumbing while the dependency evaluates at this statement's
- // exact position through its run-once initializer.
- if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) {
- if (!ts.isSourceFile(stmt.parent)) {
- lowerer.unsupported(
- "SC1090",
- decl,
- "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)",
- );
- }
- const target = createRequireProgramModuleOf(lowerer, decl.initializer)!;
- const init = lowerer.requireInitStmt(target.spec, decl);
- return init ? [init] : [];
- }
- // CommonJS require declarations (JS files): at the module's top
- // level the BINDINGS are alias plumbing (no storage;
- // resolveValueSymbol routes the reads), but the require itself is
- // Node's inline module evaluation — a relative require lowers to the
- // required module's guarded %init call at exactly this position
- // (first require runs the body, later ones are cache hits; builtins
- // load nothing). Binding requires anywhere else would need lazily-
- // initialized alias storage this model doesn't represent; named
- // fence.
- if (
- decl.initializer !== undefined &&
- requireSpecOf(decl.initializer) !== null &&
- !isNativeFfiRequire(lowerer, decl.initializer) &&
- isJsSourceFile(decl.getSourceFile())
- ) {
- const spec = requireSpecOf(decl.initializer)!;
- if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
- if (ts.isSourceFile(stmt.parent)) {
- const init = lowerer.requireInitStmt(spec, decl);
- return init ? [init] : [];
- }
- // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync }
- // = require('child_process'); ... }` — test/common's lazy-getter
- // idiom): builtins load nothing, so the statement is PURE alias
- // plumbing wherever it sits — the bindings resolve through the
- // same routing top-level requires use, and no statement remains.
- // Relative requires keep the fence: their module-init-at-position
- // semantics needs storage this model doesn't represent.
- if (canonicalBuiltinModule(spec) !== null) {
- return [];
- }
+ const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet);
+ // A require binding earlier code can read is initialized from here on
+ // (require-initialization.ts).
+ const initialized = requireInitFlagStmt(lowerer, decl);
+ return initialized ? [...lowered, initialized] : lowered;
+ });
+}
+
+function lowerVarDeclarator(
+ lowerer: Lowerer,
+ stmt: ts.VariableStatement,
+ decl: ts.VariableDeclaration,
+ isConst: boolean,
+ isLet: boolean,
+): IrStmt[] {
+ // The canonical ESM bridge to a compiled program module. Like a
+ // top-level CommonJS require declaration, the binding is alias
+ // plumbing while the dependency evaluates at this statement's
+ // exact position through its run-once initializer.
+ if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) {
+ if (!ts.isSourceFile(stmt.parent)) {
lowerer.unsupported(
"SC1090",
decl,
- "require() with bindings outside the module's top level (move it to the top of the file)",
+ "createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)",
);
}
- // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over
- // the baked constants table: alias plumbing, no storage (uses read
- // their literals via builtinConstantBindingOf; collectGlobals skipped
- // the globals by the same test).
- if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return [];
- if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
- // `const { createSign } = crypto` over a builtin NAMESPACE binding:
- // alias plumbing (builtinImportOf routes the reads) — no statement.
- if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return [];
- return lowerer.lowerDestructuringDecl(decl, isLet);
- }
- // STORED protocol values with statically closed representations:
- // numeric value iterators retain source/cursor/done hidden locals; a
- // matchAll drain retains its companion index array so a later
- // for-of can serve `m.index`. Both are const-only interceptions.
- const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst);
- if (numericIterator) return numericIterator;
- const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst);
- if (drained) return drained;
- const lowered = lowerer.lowerVarDecl(decl, isLet);
- return lowered ? [lowered] : [];
- });
+ const target = createRequireProgramModuleOf(lowerer, decl.initializer)!;
+ const init = lowerer.requireInitStmt(target.spec, decl);
+ return init ? [init] : [];
+ }
+ // CommonJS require declarations (JS files): at the module's top
+ // level the BINDINGS are alias plumbing (no storage;
+ // resolveValueSymbol routes the reads), but the require itself is
+ // Node's inline module evaluation — a relative require lowers to the
+ // required module's guarded %init call at exactly this position
+ // (first require runs the body, later ones are cache hits; builtins
+ // load nothing). Binding requires anywhere else would need lazily-
+ // initialized alias storage this model doesn't represent; named
+ // fence.
+ if (
+ decl.initializer !== undefined &&
+ requireSpecOf(decl.initializer) !== null &&
+ !isNativeFfiRequire(lowerer, decl.initializer) &&
+ !isJsonRequireDecl(lowerer, decl) &&
+ isJsSourceFile(decl.getSourceFile())
+ ) {
+ const spec = requireSpecOf(decl.initializer)!;
+ if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
+ if (ts.isSourceFile(stmt.parent)) {
+ const init = lowerer.requireInitStmt(spec, decl);
+ return init ? [init] : [];
+ }
+ // A nested BUILTIN require (`get inFreeBSDJail() { const { execSync }
+ // = require('child_process'); ... }` — test/common's lazy-getter
+ // idiom): builtins load nothing, so the statement is PURE alias
+ // plumbing wherever it sits — the bindings resolve through the
+ // same routing top-level requires use, and no statement remains.
+ // Relative requires keep the fence: their module-init-at-position
+ // semantics needs storage this model doesn't represent.
+ if (canonicalBuiltinModule(spec) !== null) {
+ return [];
+ }
+ lowerer.unsupported(
+ "SC1090",
+ decl,
+ "require() with bindings outside the module's top level (move it to the top of the file)",
+ );
+ }
+ // `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over
+ // the baked constants table: alias plumbing, no storage (uses read
+ // their literals via builtinConstantBindingOf; collectGlobals skipped
+ // the globals by the same test).
+ if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return [];
+ if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
+ // `const { createSign } = crypto` over a builtin NAMESPACE binding:
+ // alias plumbing (builtinImportOf routes the reads) — no statement.
+ if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return [];
+ return lowerer.lowerDestructuringDecl(decl, isLet);
+ }
+ // STORED protocol values with statically closed representations:
+ // numeric value iterators retain source/cursor/done hidden locals; a
+ // matchAll drain retains its companion index array so a later
+ // for-of can serve `m.index`. Both are const-only interceptions.
+ const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst);
+ if (numericIterator) return numericIterator;
+ const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst);
+ if (drained) return drained;
+ const lowered = lowerer.lowerVarDecl(decl, isLet);
+ return lowered ? [lowered] : [];
}
/** The numeric indexed source of a built-in value-iterator expression:
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 087b4e628b..e62e33894a 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")
@@ -6289,6 +6304,7 @@ export class Lowerer {
// slots), uniformly across params/locals/globals/fields/body types so
// every producer and consumer agrees. Programs with no unregistered
// reference are untouched — byte-stability holds.
+ checkRequireBindingAccesses(this, functions);
const module: IrModule | null =
this.diags.length > 0
? null
diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts
new file mode 100644
index 0000000000..5375f3792e
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/require-initialization.ts
@@ -0,0 +1,262 @@
+/* CommonJS require bindings read before their require ran. A top-level
+ * `const x = require("./dep")` binds x AT the require: Node throws "Cannot
+ * access 'x' before initialization" when code that runs earlier (a hoisted
+ * function called above the require, say) reads it. The lowering keeps
+ * require bindings as module aliases, so the reads themselves cannot
+ * observe the order. When preflight finds code that can reach a binding
+ * before its require (requireEarlyReference), the declaration gets a BOOL
+ * initialization flag, set right after the require evaluated the module,
+ * and every reference that can run earlier checks it first — the same
+ * module.tdzCheck the import-cycle bindings use. A reference whose lowering
+ * leaves no expression to check keeps the SC1013 fence. */
+import * as ts from "../ts7/adapter.js";
+import type { Lowerer } from "./lowerer.js";
+import {
+ inDeferredPosition,
+ isCjsJsFile,
+ locOf,
+ requireEarlyReference,
+ requireSpecOf,
+} from "../program.js";
+import { unsupportedDiag } from "../../diagnostics/diagnostic.js";
+import {
+ BOOL,
+ STRING,
+ VOID,
+ type IrExpr,
+ type IrFunction,
+ type IrStmt,
+ type SrcLoc,
+} from "../../ir/ir.js";
+import { mapExprChildren, mapStmtChildren } from "../../ir/traverse.js";
+
+/** One flagged require declaration. */
+export interface RequireInitFlag {
+ flagId: string;
+ /** The binding names, by symbol. */
+ names: Map;
+}
+
+/** A reference that can run before its require: the identifier, the
+ * outermost expression whose evaluation starts with reading it, and the
+ * body of the innermost function holding it (null at the top level). */
+export interface EarlyReference {
+ flag: RequireInitFlag;
+ name: string;
+ ref: SrcLoc;
+ chain: SrcLoc;
+ body: SrcLoc | null;
+ node: ts.Identifier;
+}
+
+function boundNames(name: ts.BindingName): ts.Identifier[] {
+ if (ts.isIdentifier(name)) return [name];
+ const out: ts.Identifier[] = [];
+ for (const el of name.elements) {
+ if (ts.isOmittedExpression(el) || el.name === undefined) continue;
+ out.push(...boundNames(el.name));
+ }
+ return out;
+}
+
+/** The outermost node whose evaluation begins by evaluating `ref`: member
+ * reads, calls and constructions through it, and the transparent wrappers
+ * around them. */
+function chainOf(ref: ts.Identifier): ts.Node {
+ let node: ts.Node = ref;
+ for (;;) {
+ const parent = node.parent;
+ if (parent === undefined) return node;
+ if (
+ ((ts.isPropertyAccessExpression(parent) ||
+ ts.isElementAccessExpression(parent) ||
+ ts.isCallExpression(parent) ||
+ ts.isNewExpression(parent)) &&
+ parent.expression === node) ||
+ (ts.isTaggedTemplateExpression(parent) && parent.tag === node) ||
+ ts.isParenthesizedExpression(parent) ||
+ ts.isNonNullExpression(parent) ||
+ (ts.isAsExpression(parent) && parent.expression === node) ||
+ (ts.isSatisfiesExpression(parent) && parent.expression === node)
+ ) {
+ node = parent;
+ continue;
+ }
+ return node;
+ }
+}
+
+function enclosingBody(node: ts.Node): ts.Node | null {
+ for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
+ if (ts.isFunctionLike(p)) {
+ const body = (p as { body?: ts.Node }).body;
+ if (body !== undefined) return body;
+ }
+ if (ts.isPropertyDeclaration(p) && p.initializer !== undefined) return p.initializer;
+ }
+ return null;
+}
+
+/** Registers a flag for every top-level const/let require declaration of a
+ * CommonJS module whose bindings earlier code can reach, and collects the
+ * references that can run before the require. Runs once per lowering pass,
+ * after global collection. */
+export function markRequireEarlyBindings(lowerer: Lowerer): void {
+ for (const sf of lowerer.moduleOrder) {
+ if (sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) continue;
+ const declared: { decl: ts.VariableDeclaration; stmt: ts.VariableStatement }[] = [];
+ for (const stmt of sf.statements) {
+ if (!ts.isVariableStatement(stmt)) continue;
+ if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Let)) === 0) continue;
+ for (const decl of stmt.declarationList.declarations) {
+ if (decl.initializer === undefined || requireSpecOf(decl.initializer) === null) continue;
+ if (requireEarlyReference(lowerer.program, decl) === null) continue;
+ declared.push({ decl, stmt });
+ }
+ }
+ if (declared.length === 0) continue;
+ const rawTag = lowerer.fileTag.get(sf) ?? "";
+ const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, "");
+ const bySymbol = new Map();
+ for (const { decl } of declared) {
+ const flagId = `%g.${tag}%required.${decl.getStart(sf)}`;
+ lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
+ const flag: RequireInitFlag = { flagId, names: new Map() };
+ for (const id of boundNames(decl.name)) {
+ const symbol = lowerer.checker.getSymbolAtLocation(id);
+ if (symbol === undefined) continue;
+ flag.names.set(symbol, id.text);
+ bySymbol.set(symbol, { flag, decl });
+ }
+ lowerer.requireInitFlags.set(decl, flag);
+ }
+ const names = new Set();
+ for (const { flag } of bySymbol.values()) for (const n of flag.names.values()) names.add(n);
+ const idents: ts.Identifier[] = [];
+ ts.walkPreorder(sf, (node) => {
+ if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node);
+ return undefined;
+ });
+ lowerer.checker.prefetchSymbolNodesExact(idents);
+ for (const id of idents) {
+ const parent = id.parent;
+ const symbol =
+ parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id
+ ? lowerer.checker.getShorthandAssignmentValueSymbol(parent)
+ : lowerer.checker.getSymbolAtLocation(id);
+ const owner = symbol === undefined ? undefined : bySymbol.get(symbol);
+ if (owner === undefined) continue;
+ if (boundNames(owner.decl.name).includes(id)) continue;
+ // Code after the require runs after it, except inside functions.
+ if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue;
+ const body = enclosingBody(id);
+ lowerer.requireEarlyReferences.push({
+ flag: owner.flag,
+ name: owner.flag.names.get(symbol!)!,
+ ref: locOf(id),
+ chain: locOf(chainOf(id)),
+ body: body === null ? null : locOf(body),
+ node: id,
+ });
+ }
+ }
+}
+
+/** The statement marking a flagged require declaration initialized, placed
+ * right after its require evaluated the module. */
+export function requireInitFlagStmt(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null {
+ const flag = lowerer.requireInitFlags.get(decl);
+ if (flag === undefined) return null;
+ const loc = locOf(decl);
+ return {
+ kind: "assign",
+ localId: flag.flagId,
+ value: { kind: "boolLit", value: true, type: BOOL, loc },
+ loc,
+ };
+}
+
+const within = (inner: SrcLoc, outer: SrcLoc): boolean =>
+ inner.file === outer.file && outer.start <= inner.start && inner.end <= outer.end;
+
+/** Inserts the initialization check before the outermost expression built
+ * for each early reference, and refuses a reference whose lowered code
+ * holds no expression for it. */
+export function checkRequireBindingAccesses(lowerer: Lowerer, functions: IrFunction[]): void {
+ const refs = lowerer.requireEarlyReferences;
+ if (refs.length === 0) return;
+ const byFile = new Map();
+ for (const r of refs) {
+ const list = byFile.get(r.ref.file);
+ if (list) list.push(r);
+ else byFile.set(r.ref.file, [r]);
+ }
+ const covered = new Set();
+ const loweredBodies = new Set();
+ const claimed = new Set();
+ const check = (r: EarlyReference, loc: SrcLoc): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "intrinsic",
+ name: "module.tdzCheck",
+ args: [
+ { kind: "varRef", localId: r.flag.flagId, type: BOOL, loc },
+ { kind: "strLit", value: r.name, type: STRING, loc },
+ ],
+ type: VOID,
+ loc,
+ },
+ loc,
+ });
+ const note = (loc: SrcLoc | undefined): EarlyReference[] => {
+ if (loc === undefined) return [];
+ const list = byFile.get(loc.file);
+ if (list === undefined) return [];
+ const mine: EarlyReference[] = [];
+ for (const r of list) {
+ if (r.body !== null && !loweredBodies.has(r) && within(loc, r.body)) loweredBodies.add(r);
+ if (!claimed.has(r) && within(r.ref, loc) && within(loc, r.chain)) mine.push(r);
+ }
+ return mine;
+ };
+ const rewriteExpr = (e: IrExpr): IrExpr => {
+ const mine = note(e.loc);
+ for (const r of mine) claimed.add(r);
+ const out = mapExprChildren(e, rewriteExpr, rewriteStmt);
+ for (const r of mine) {
+ claimed.delete(r);
+ covered.add(r);
+ }
+ if (mine.length === 0) return out;
+ const flags = new Set();
+ const stmts: IrStmt[] = [];
+ for (const r of mine) {
+ if (flags.has(r.flag.flagId)) continue;
+ flags.add(r.flag.flagId);
+ stmts.push(check(r, e.loc));
+ }
+ return { kind: "seqExpr", stmts, result: out, type: out.type, loc: out.loc };
+ };
+ const rewriteStmt = (s: IrStmt): IrStmt => {
+ note(s.loc);
+ return mapStmtChildren(s, rewriteExpr, rewriteStmt);
+ };
+ for (const fn of functions) fn.body = fn.body.map(rewriteStmt);
+ if (lowerer.remainder) return;
+ const reported = new Set();
+ for (const r of refs) {
+ if (covered.has(r)) continue;
+ // A function body that never lowered never runs.
+ if (r.body !== null && !loweredBodies.has(r)) continue;
+ const key = `${r.ref.file}:${r.ref.start}`;
+ if (reported.has(key)) continue;
+ reported.add(key);
+ lowerer.pushDiag(
+ unsupportedDiag(
+ "SC1013",
+ r.ref,
+ `reading require() binding '${r.name}' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first)`,
+ ),
+ );
+ }
+}
diff --git a/packages/compiler/src/frontend/program-analysis.ts b/packages/compiler/src/frontend/program-analysis.ts
index 0ecf09e6a7..e49b9307e4 100644
--- a/packages/compiler/src/frontend/program-analysis.ts
+++ b/packages/compiler/src/frontend/program-analysis.ts
@@ -1,4 +1,9 @@
-import type { ExportDeclaration, SourceFile, VariableDeclaration } from "./ts7/ast-types.js";
+import type {
+ ExportDeclaration,
+ Identifier,
+ SourceFile,
+ VariableDeclaration,
+} from "./ts7/ast-types.js";
import type { ModuleSourceCandidates } from "./module-source-candidates.js";
import type { CycleEarlyBinding } from "./program.js";
@@ -20,6 +25,11 @@ export class ProgramAnalysis {
readonly exactCycleMembers = new Set();
cycleEarlyBindings: EarlyBindingsMemo | null = null;
moduleEarlyBindings: EarlyBindingsMemo | null = null;
+ /** Top-level require declarations: the first reference to their bindings
+ * that code running before the require can reach (requireEarlyReference). */
+ readonly requireEarlyReferences = new Map();
+ /** Per file: the first top-level statement that can run user code. */
+ readonly requireFirstRunnable = new Map();
clear(): void {
this.createRequireReasons.clear();
@@ -29,5 +39,7 @@ export class ProgramAnalysis {
this.exactCycleMembers.clear();
this.cycleEarlyBindings = null;
this.moduleEarlyBindings = null;
+ this.requireEarlyReferences.clear();
+ this.requireFirstRunnable.clear();
}
}
diff --git a/packages/compiler/src/frontend/program.test.ts b/packages/compiler/src/frontend/program.test.ts
index 33abf6d679..8fec2f486f 100644
--- a/packages/compiler/src/frontend/program.test.ts
+++ b/packages/compiler/src/frontend/program.test.ts
@@ -126,10 +126,12 @@ test.for([
["replacement export property", "module.exports = read; module.exports.name = read;"],
["non-export assignment chain", "const box = {}; exports.alias = box.read = read;"],
])("require still refuses an early read through %s", ([, early]) => {
+ // A var binding keeps the fence (Node reads undefined there); a const
+ // compiles with initialization checks instead, from the same analysis.
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
${early}
-const dep = require('./dep.cjs');
+var dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(
@@ -140,13 +142,43 @@ function read() { return dep.value; }
test("publishing a require binding itself still reads it before initialization", () => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
-const { read } = require('./dep.cjs');
+var { read } = require('./dep.cjs');
`);
expect(
diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'read'")),
).toBe(true);
});
+test("an early-reachable const require binding compiles with initialization checks", () => {
+ // Preflight admits it; the lowering checks every reference that can run
+ // before the require (require-initialization.ts).
+ expect(
+ requireOrderDiagnostics(`
+exports.read = read;
+read();
+const dep = require('./dep.cjs');
+function read() { return dep.value; }
+`),
+ ).toEqual([]);
+});
+
+test.for([
+ ["a class whose methods read it", "class Reader { read() { return dep.value; } }"],
+ ["a stored class expression", "var Reader = class { read() { return dep.value; } };"],
+ ["a later-job callback", "setTimeout(() => dep.value, 0);"],
+ ["a published getter", "Object.defineProperty(exports, 'value', { get: () => dep.value });"],
+ ["a property stored on a published function", "module.exports = read; read.again = again;"],
+])("code that cannot run before the require does not reach it: %s", ([, early]) => {
+ expect(
+ requireOrderDiagnostics(`
+${early}
+var dep = require('./dep.cjs');
+function read() { return dep.value; }
+function again() { return dep.value; }
+`),
+ ).toEqual([]);
+});
+
test.for([
"var Object = null; Object.defineProperty(exports, '__esModule', { value: true }); exports.read = read;",
"var exports = null; exports.read = read;",
@@ -154,7 +186,7 @@ test.for([
])("source bindings cannot masquerade as a CommonJS prologue: %s", (prefix) => {
const diagnostics = requireOrderDiagnostics(`
${prefix}
-const dep = require('./dep.cjs');
+var dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(
@@ -165,7 +197,7 @@ function read() { return dep.value; }
test("an earlier declarator can call an export before the require initializes", () => {
const diagnostics = requireOrderDiagnostics(`
exports.read = read;
-const before = read(), dep = require('./dep.cjs');
+var before = read(), dep = require('./dep.cjs');
function read() { return dep.value; }
`);
expect(
@@ -183,6 +215,63 @@ function read() { return dep.value; }
).toEqual([]);
});
+test("a member added to a replaced export does not run the replacement (on-finished)", () => {
+ expect(
+ requireOrderDiagnostics(`
+module.exports = read;
+module.exports.check = check;
+var hooks = probe();
+var dep = require('./dep.cjs');
+function read() { return dep.value; }
+function check() { return 1; }
+function probe() { return check(); }
+`),
+ ).toEqual([]);
+});
+
+test("a require cycle whose modules export first and use each other only later is admitted", () => {
+ expect(
+ requireOrderDiagnostics(
+ `
+const ANY = Symbol('ANY');
+class Main { dep() { return new Dep(); } }
+module.exports = Main;
+const Dep = require('./dep.cjs');
+`,
+ "class Dep { main() { return new Main(); } }\nmodule.exports = Dep;\nconst Main = require('./main.cjs');\n",
+ ),
+ ).toEqual([]);
+});
+
+test.for([
+ [
+ "an export after the require",
+ "const Main = require('./main.cjs');\nmodule.exports = { Dep: 1 };\n",
+ ],
+ [
+ "a top-level use of the cycle binding",
+ "module.exports = 1;\nconst Main = require('./main.cjs');\nnew Main();\n",
+ ],
+ [
+ "an escaping export object",
+ "module.exports = 1;\nconst Main = require('./main.cjs');\nObject.assign(exports, {});\n",
+ ],
+ [
+ "a call before the export",
+ "setup();\nmodule.exports = 1;\nconst Main = require('./main.cjs');\nfunction setup() {}\n",
+ ],
+])("a require cycle keeps SC1016 for %s", ([, dependency]) => {
+ const diagnostics = requireOrderDiagnostics(
+ `
+class Main {}
+module.exports = Main;
+const dep = require('./dep.cjs');
+`,
+ dependency,
+ );
+ expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true);
+});
+
test("function publication does not admit CommonJS cycles", () => {
const diagnostics = requireOrderDiagnostics(
`
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index 3a4ec16479..df0cb673e2 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/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json
new file mode 100644
index 0000000000..db768b31be
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6140-cjs-require-tdz-throws/main.js.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6140-cjs-require-tdz-throws/lib.js",
+ "/tests/corpus/6140-cjs-require-tdz-throws/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json
new file mode 100644
index 0000000000..b074bf18d6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6141-cjs-require-tdz-checked/main.js.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/corpus/6141-cjs-require-tdz-checked/lib.js",
+ "/tests/corpus/6141-cjs-require-tdz-checked/counter.js",
+ "/tests/corpus/6141-cjs-require-tdz-checked/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json
new file mode 100644
index 0000000000..55720ef1a4
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6142-cjs-require-published-early/main.js.json
@@ -0,0 +1,13 @@
+{
+ "order": [
+ "/tests/corpus/6142-cjs-require-published-early/parse-options.js",
+ "/tests/corpus/6142-cjs-require-published-early/debug.js",
+ "/tests/corpus/6142-cjs-require-published-early/comparator.js",
+ "/tests/corpus/6142-cjs-require-published-early/helpers.js",
+ "/tests/corpus/6142-cjs-require-published-early/glob.js",
+ "/tests/corpus/6142-cjs-require-published-early/core.js",
+ "/tests/corpus/6142-cjs-require-published-early/bundle.js",
+ "/tests/corpus/6142-cjs-require-published-early/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json
new file mode 100644
index 0000000000..92fe169c31
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6143-cjs-require-json/main.js.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6143-cjs-require-json/counter.js",
+ "/tests/corpus/6143-cjs-require-json/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json
new file mode 100644
index 0000000000..df78ca9ed9
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6144-cjs-require-builtin-nested/main.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6144-cjs-require-builtin-nested/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json
new file mode 100644
index 0000000000..3e8122d1f2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6145-cjs-require-cycle/main.js.json
@@ -0,0 +1,9 @@
+{
+ "order": [
+ "/tests/corpus/6145-cjs-require-cycle/range.js",
+ "/tests/corpus/6145-cjs-require-cycle/util.js",
+ "/tests/corpus/6145-cjs-require-cycle/comparator.js",
+ "/tests/corpus/6145-cjs-require-cycle/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json
index a54923bcfe..67d01f47e2 100644
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-cycle/main.js.json
@@ -7,7 +7,7 @@
"diags": [
{
"code": "SC1016",
- "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet",
+ "message": "circular imports (/tests/diagnostics/cjs-require-cycle/a.js → /tests/diagnostics/cjs-require-cycle/b.js → /tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge: /tests/diagnostics/cjs-require-cycle/a.js uses its exports at /tests/diagnostics/cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet",
"loc": {
"file": "/tests/diagnostics/cjs-require-cycle/a.js",
"start": 0,
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json
new file mode 100644
index 0000000000..aaeea4cf27
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz-unchecked/main.js.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/diagnostics/cjs-require-tdz-unchecked/lib.js",
+ "/tests/diagnostics/cjs-require-tdz-unchecked/main.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json
index d15d404ae9..205a8cd6ee 100644
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/cjs-require-tdz/main.js.json
@@ -5,11 +5,11 @@
"diags": [
{
"code": "SC1013",
- "message": "require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet",
+ "message": "var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet",
"loc": {
"file": "/tests/diagnostics/cjs-require-tdz/main.js",
- "start": 336,
- "end": 367
+ "start": 402,
+ "end": 427
},
"milestone": "later"
}
diff --git a/packages/compiler/test/ts7/program-adapter.test.ts b/packages/compiler/test/ts7/program-adapter.test.ts
index 660394a6fd..e7479c7498 100644
--- a/packages/compiler/test/ts7/program-adapter.test.ts
+++ b/packages/compiler/test/ts7/program-adapter.test.ts
@@ -119,7 +119,7 @@ ${uses}
return required.value + value;
}
before();
-const required = require("./dep.cjs");
+var required = require("./dep.cjs");
console.log(required.value);
`,
);
@@ -144,6 +144,8 @@ console.log(required.value);
}) as Checker;
program.checkerFacade = new CheckerFacade(proxy, { project: program.project.checker.project });
+ // A var binding keeps the SC1013 fence (Node reads undefined there); a
+ // const would compile with initialization checks from the same scan.
expect(checkPreflight(load).map((diag) => diag.code)).toContain("SC1013");
expect(calls.some(([arg]) => Array.isArray(arg) && arg.length > 24)).toBe(true);
expect(calls.some(([arg]) => !Array.isArray(arg))).toBe(false);
diff --git a/tests/corpus/6140-cjs-require-tdz-throws/lib.js b/tests/corpus/6140-cjs-require-tdz-throws/lib.js
new file mode 100644
index 0000000000..2a9669e123
--- /dev/null
+++ b/tests/corpus/6140-cjs-require-tdz-throws/lib.js
@@ -0,0 +1,8 @@
+'use strict';
+
+/** @param {string} who */
+function greet(who) {
+ return `hello ${who}`;
+}
+
+module.exports = { greet };
diff --git a/tests/corpus/6140-cjs-require-tdz-throws/main.js b/tests/corpus/6140-cjs-require-tdz-throws/main.js
new file mode 100644
index 0000000000..13163137e2
--- /dev/null
+++ b/tests/corpus/6140-cjs-require-tdz-throws/main.js
@@ -0,0 +1,15 @@
+// @exit: 1
+// A hoisted function called above the require reads the binding before
+// the require ran: Node throws "Cannot access 'greet' before
+// initialization" there, uncaught, and nothing after it runs.
+'use strict';
+
+console.log('start');
+console.log('boot', helper());
+const { greet } = require('./lib.js');
+
+function helper() {
+ return greet('early');
+}
+
+console.log('unreachable', helper());
diff --git a/tests/corpus/6140-cjs-require-tdz-throws/package.json b/tests/corpus/6140-cjs-require-tdz-throws/package.json
new file mode 100644
index 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6140-cjs-require-tdz-throws/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6141-cjs-require-tdz-checked/counter.js b/tests/corpus/6141-cjs-require-tdz-checked/counter.js
new file mode 100644
index 0000000000..24ef13e5de
--- /dev/null
+++ b/tests/corpus/6141-cjs-require-tdz-checked/counter.js
@@ -0,0 +1,14 @@
+'use strict';
+
+class Counter {
+ /** @param {number} start */
+ constructor(start) {
+ this.value = start;
+ }
+ next() {
+ this.value += 1;
+ return this.value;
+ }
+}
+
+module.exports = Counter;
diff --git a/tests/corpus/6141-cjs-require-tdz-checked/lib.js b/tests/corpus/6141-cjs-require-tdz-checked/lib.js
new file mode 100644
index 0000000000..aa15c97d6a
--- /dev/null
+++ b/tests/corpus/6141-cjs-require-tdz-checked/lib.js
@@ -0,0 +1,8 @@
+'use strict';
+
+/** @param {string} who */
+function greet(who) {
+ return `hello ${who}`;
+}
+
+module.exports = { greet, answer: 42 };
diff --git a/tests/corpus/6141-cjs-require-tdz-checked/main.js b/tests/corpus/6141-cjs-require-tdz-checked/main.js
new file mode 100644
index 0000000000..917eb78743
--- /dev/null
+++ b/tests/corpus/6141-cjs-require-tdz-checked/main.js
@@ -0,0 +1,49 @@
+// Require bindings that code running before the require can read. Every
+// read that happens before the require throws Node's ReferenceError for
+// the local binding name, before any argument of the access evaluates;
+// the same reads succeed once the require ran.
+'use strict';
+
+/**
+ * @param {string} label
+ * @param {() => unknown} read
+ */
+function attempt(label, read) {
+ try {
+ console.log(label, 'ok', read());
+ } catch (e) {
+ if (!(e instanceof ReferenceError)) throw e;
+ console.log(label, e.name, e.message);
+ }
+}
+
+/** @param {string} s */
+function note(s) {
+ console.log('evaluated', s);
+ return s;
+}
+
+const later = [() => greet('later'), () => new Counter(5).next()];
+
+attempt('call', () => greet('a'));
+attempt('call-arguments', () => greet(note('argument')));
+attempt('member', () => lib.greet('b'));
+attempt('construct', () => new Counter(1).next());
+attempt('builtin', () => path.join('a', 'b'));
+attempt('value', () => answer + 1);
+attempt('renamed', () => hello('c'));
+
+const { greet } = require('./lib.js');
+const lib = require('./lib.js');
+const Counter = require('./counter.js');
+const path = require('path');
+const { answer, greet: hello } = require('./lib.js');
+
+attempt('call', () => greet('a'));
+attempt('call-arguments', () => greet(note('argument')));
+attempt('member', () => lib.greet('b'));
+attempt('construct', () => new Counter(1).next());
+attempt('builtin', () => path.join('a', 'b'));
+attempt('value', () => answer + 1);
+attempt('renamed', () => hello('c'));
+for (const read of later) console.log('later', read());
diff --git a/tests/corpus/6141-cjs-require-tdz-checked/package.json b/tests/corpus/6141-cjs-require-tdz-checked/package.json
new file mode 100644
index 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6141-cjs-require-tdz-checked/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6142-cjs-require-published-early/bundle.js b/tests/corpus/6142-cjs-require-published-early/bundle.js
new file mode 100644
index 0000000000..c123872197
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/bundle.js
@@ -0,0 +1,17 @@
+'use strict';
+
+// The esbuild CommonJS layout: stored class expressions and the export
+// table come before the var requires their methods use. Publishing the
+// classes runs none of their methods.
+const Provider = class {
+ /** @param {string} name */
+ constructor(name) {
+ this.name = name;
+ }
+ describe() {
+ return core.label(this.name);
+ }
+};
+const REASON = 'default';
+module.exports = { Provider, REASON };
+var core = require('./core.js');
diff --git a/tests/corpus/6142-cjs-require-published-early/comparator.js b/tests/corpus/6142-cjs-require-published-early/comparator.js
new file mode 100644
index 0000000000..2b2ea6146d
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/comparator.js
@@ -0,0 +1,37 @@
+'use strict';
+
+// The semver layout: the class and its export come first, the requires its
+// methods use come after. Publishing the class runs none of its methods, so
+// no read can happen before the requires ran.
+const ANY = Symbol('ANY');
+
+class Comparator {
+ static get ANY() {
+ return ANY;
+ }
+
+ static describe() {
+ return `Comparator(${typeof parseOptions})`;
+ }
+
+ /**
+ * @param {string} text
+ * @param {{ loose?: boolean }} [options]
+ */
+ constructor(text, options) {
+ this.mode = parseOptions(options);
+ this.text = text.trim();
+ debug('comparator', this.text, this.mode);
+ }
+
+ /** @param {string} other */
+ test(other) {
+ return path.basename(other) === this.text;
+ }
+}
+
+module.exports = Comparator;
+
+const parseOptions = require('./parse-options.js');
+const { debug } = require('./debug.js');
+const path = require('path');
diff --git a/tests/corpus/6142-cjs-require-published-early/core.js b/tests/corpus/6142-cjs-require-published-early/core.js
new file mode 100644
index 0000000000..9e3a9a565c
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/core.js
@@ -0,0 +1,10 @@
+'use strict';
+
+console.log('core: init');
+
+/** @param {string} name */
+function label(name) {
+ return `<${name}>`;
+}
+
+module.exports = { label };
diff --git a/tests/corpus/6142-cjs-require-published-early/debug.js b/tests/corpus/6142-cjs-require-published-early/debug.js
new file mode 100644
index 0000000000..30451569b1
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/debug.js
@@ -0,0 +1,10 @@
+'use strict';
+
+console.log('debug: init');
+
+/** @param {...unknown} args */
+function debug(...args) {
+ console.log('debug:', args.join(' '));
+}
+
+module.exports = { debug };
diff --git a/tests/corpus/6142-cjs-require-published-early/glob.js b/tests/corpus/6142-cjs-require-published-early/glob.js
new file mode 100644
index 0000000000..3d2907f005
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/glob.js
@@ -0,0 +1,20 @@
+'use strict';
+
+// The glob@7 layout with var: export first, then var requires, a runnable
+// statement between them, and properties stored on the exported function.
+module.exports = glob;
+glob.sync = globSync;
+
+var parseOptions = require('./parse-options.js');
+var mode = parseOptions({ loose: true });
+var helpers = require('./helpers.js');
+
+/** @param {string} pattern */
+function glob(pattern) {
+ return `${mode}:${helpers.normalize(pattern)}`;
+}
+
+/** @param {string} pattern */
+function globSync(pattern) {
+ return helpers.normalize(pattern).toUpperCase();
+}
diff --git a/tests/corpus/6142-cjs-require-published-early/helpers.js b/tests/corpus/6142-cjs-require-published-early/helpers.js
new file mode 100644
index 0000000000..206ca77851
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/helpers.js
@@ -0,0 +1,10 @@
+'use strict';
+
+console.log('helpers: init');
+
+/** @param {string} s */
+function normalize(s) {
+ return s.replace(/\/+/g, '/');
+}
+
+module.exports = { normalize };
diff --git a/tests/corpus/6142-cjs-require-published-early/main.js b/tests/corpus/6142-cjs-require-published-early/main.js
new file mode 100644
index 0000000000..310dacdb01
--- /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 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6142-cjs-require-published-early/parse-options.js b/tests/corpus/6142-cjs-require-published-early/parse-options.js
new file mode 100644
index 0000000000..0872049ea3
--- /dev/null
+++ b/tests/corpus/6142-cjs-require-published-early/parse-options.js
@@ -0,0 +1,8 @@
+'use strict';
+
+/** @param {{ loose?: boolean } | undefined} options */
+function parseOptions(options) {
+ return options && options.loose ? 'loose' : 'strict';
+}
+
+module.exports = parseOptions;
diff --git a/tests/corpus/6143-cjs-require-json/counter.js b/tests/corpus/6143-cjs-require-json/counter.js
new file mode 100644
index 0000000000..b794d1d4c8
--- /dev/null
+++ b/tests/corpus/6143-cjs-require-json/counter.js
@@ -0,0 +1,14 @@
+'use strict';
+
+// A second requirer: it receives the same cached object and mutates it.
+const data = require('./data.json');
+
+console.log('counter: sees', data.count);
+data.count += 1;
+
+function bump() {
+ data.count += 100;
+ data.tags.push('bumped');
+}
+
+module.exports = { bump };
diff --git a/tests/corpus/6143-cjs-require-json/data.json b/tests/corpus/6143-cjs-require-json/data.json
new file mode 100644
index 0000000000..b1cf3d3bc9
--- /dev/null
+++ b/tests/corpus/6143-cjs-require-json/data.json
@@ -0,0 +1,7 @@
+{
+ "name": "fixture-data",
+ "version": "1.2.3",
+ "count": 41,
+ "tags": ["native", "json"],
+ "nested": { "flag": true, "ratio": 0.5 }
+}
diff --git a/tests/corpus/6143-cjs-require-json/list.json b/tests/corpus/6143-cjs-require-json/list.json
new file mode 100644
index 0000000000..98ace19c09
--- /dev/null
+++ b/tests/corpus/6143-cjs-require-json/list.json
@@ -0,0 +1 @@
+[3, 1, 2]
diff --git a/tests/corpus/6143-cjs-require-json/main.js b/tests/corpus/6143-cjs-require-json/main.js
new file mode 100644
index 0000000000..b67c39f3eb
--- /dev/null
+++ b/tests/corpus/6143-cjs-require-json/main.js
@@ -0,0 +1,27 @@
+// require() of a JSON document: Node parses it at the first require and
+// caches the object, so every require of the document, from any module,
+// returns the same object and a mutation is visible to all requirers.
+// Destructuring copies the properties at the require.
+'use strict';
+
+const data = require('./data.json');
+const { count, nested: { flag, ratio }, tags: [firstTag] } = require('./data.json');
+const version = require('./data.json').version;
+
+console.log(data.name, version, data.count, count, flag, ratio, firstTag);
+console.log('same object', require('./data.json') === data);
+
+const { bump } = require('./counter.js');
+console.log('after counter', data.count, count);
+bump();
+console.log('after bump', data.count, data.tags.join(','), count);
+
+function load() {
+ return require('./data.json');
+}
+console.log('lazy same object', load() === data, load().count);
+load().nested.flag = false;
+console.log('flag', data.nested.flag, flag);
+
+const list = require('./list.json');
+console.log(list.length, list.slice().sort().join(','), require('./list.json') === list);
diff --git a/tests/corpus/6143-cjs-require-json/package.json b/tests/corpus/6143-cjs-require-json/package.json
new file mode 100644
index 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6143-cjs-require-json/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6144-cjs-require-builtin-nested/main.js b/tests/corpus/6144-cjs-require-builtin-nested/main.js
new file mode 100644
index 0000000000..3058767623
--- /dev/null
+++ b/tests/corpus/6144-cjs-require-builtin-nested/main.js
@@ -0,0 +1,15 @@
+// Nested destructuring of a builtin require (undici's `const { types: {
+// isPromise } } = require('util')`): util.types IS the util/types module,
+// so the nested pattern binds that module's members.
+'use strict';
+
+const {
+ types: { isPromise, isPromise: promiseCheck },
+ format,
+} = require('util');
+const { promises: { readFile } } = require('fs');
+const { posix: { join } } = require('path');
+
+console.log(isPromise(Promise.resolve(1)), isPromise({}), promiseCheck(Promise.reject(new Error('x')).catch(() => {})));
+console.log(format('%s=%d', 'a', 1), join('a', 'b', '..', 'c'));
+readFile(__filename, 'utf8').then((text) => console.log('read', text.includes('isPromise')));
diff --git a/tests/corpus/6144-cjs-require-builtin-nested/package.json b/tests/corpus/6144-cjs-require-builtin-nested/package.json
new file mode 100644
index 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6144-cjs-require-builtin-nested/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6145-cjs-require-cycle/comparator.js b/tests/corpus/6145-cjs-require-cycle/comparator.js
new file mode 100644
index 0000000000..1f0d4eb9d2
--- /dev/null
+++ b/tests/corpus/6145-cjs-require-cycle/comparator.js
@@ -0,0 +1,29 @@
+'use strict';
+
+// semver's layout: the class and its export come first ("hoisted class for
+// cyclic dependency"), the requires — one of them closing the cycle with
+// range.js — after it. When range.js requires this module mid-evaluation,
+// Node hands it the exports as they stand: already the class.
+const ANY = Symbol('ANY');
+
+class Comparator {
+ /** @param {string} value */
+ constructor(value) {
+ this.value = value;
+ }
+
+ /** @param {string} other */
+ within(other) {
+ return new Range(other).has(this.value);
+ }
+
+ static get ANY() {
+ return ANY;
+ }
+}
+
+module.exports = Comparator;
+
+const Range = require('./range.js');
+const { trim } = require('./util.js');
+console.log('comparator: loaded', trim(' done '));
diff --git a/tests/corpus/6145-cjs-require-cycle/main.js b/tests/corpus/6145-cjs-require-cycle/main.js
new file mode 100644
index 0000000000..90cbc88247
--- /dev/null
+++ b/tests/corpus/6145-cjs-require-cycle/main.js
@@ -0,0 +1,15 @@
+// A require cycle (comparator.js ↔ range.js) whose modules settle their
+// exports before they require anything, and use each other only inside
+// methods: the cycle evaluates in Node's order and each binding is the
+// other module's class.
+'use strict';
+
+console.log('main: start');
+const Comparator = require('./comparator.js');
+const Range = require('./range.js');
+
+const range = new Range('1.0.0 2.0.0');
+console.log(range.has('2.0.0'), range.has('3.0.0'));
+const first = range.first();
+console.log(first instanceof Comparator, first.value, first.within('2.0.0 1.0.0'));
+console.log(new Comparator('9').within('1 2'), String(Comparator.ANY));
diff --git a/tests/corpus/6145-cjs-require-cycle/package.json b/tests/corpus/6145-cjs-require-cycle/package.json
new file mode 100644
index 0000000000..a3c15a7a63
--- /dev/null
+++ b/tests/corpus/6145-cjs-require-cycle/package.json
@@ -0,0 +1 @@
+{ "type": "commonjs" }
diff --git a/tests/corpus/6145-cjs-require-cycle/range.js b/tests/corpus/6145-cjs-require-cycle/range.js
new file mode 100644
index 0000000000..ab71e4a52d
--- /dev/null
+++ b/tests/corpus/6145-cjs-require-cycle/range.js
@@ -0,0 +1,24 @@
+'use strict';
+
+const SEPARATOR = /\s+/;
+
+class Range {
+ /** @param {string} spec */
+ constructor(spec) {
+ this.parts = spec.split(SEPARATOR).map((part) => new Comparator(part));
+ }
+
+ /** @param {string} value */
+ has(value) {
+ return this.parts.some((part) => part.value === value);
+ }
+
+ first() {
+ return this.parts[0];
+ }
+}
+
+module.exports = Range;
+
+const Comparator = require('./comparator.js');
+console.log('range: loaded');
diff --git a/tests/corpus/6145-cjs-require-cycle/util.js b/tests/corpus/6145-cjs-require-cycle/util.js
new file mode 100644
index 0000000000..4e13760cd4
--- /dev/null
+++ b/tests/corpus/6145-cjs-require-cycle/util.js
@@ -0,0 +1,10 @@
+'use strict';
+
+console.log('util: loaded');
+
+/** @param {string} s */
+function trim(s) {
+ return s.trim();
+}
+
+module.exports = { trim };
diff --git a/tests/diagnostics/cjs-require-cycle/main.js b/tests/diagnostics/cjs-require-cycle/main.js
index 619117b910..376aa0a363 100644
--- a/tests/diagnostics/cjs-require-cycle/main.js
+++ b/tests/diagnostics/cjs-require-cycle/main.js
@@ -1,6 +1,8 @@
-// A require cycle: Node hands B the PARTIAL exports of A mid-init, a
+// A require cycle whose module exports only after requiring into the
+// cycle: Node hands B the PARTIAL exports of A mid-init, a
// partially-initialized-module observation the export-global model cannot
-// represent — cycles keep the named SC1016 fence.
+// represent — the cycle keeps the named SC1016 fence. (A cycle whose
+// modules export first compiles: tests/corpus/6145-cjs-require-cycle.)
'use strict';
console.log('main: start');
diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/lib.js b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js
new file mode 100644
index 0000000000..2a9669e123
--- /dev/null
+++ b/tests/diagnostics/cjs-require-tdz-unchecked/lib.js
@@ -0,0 +1,8 @@
+'use strict';
+
+/** @param {string} who */
+function greet(who) {
+ return `hello ${who}`;
+}
+
+module.exports = { greet };
diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/main.js b/tests/diagnostics/cjs-require-tdz-unchecked/main.js
new file mode 100644
index 0000000000..52b12fcb47
--- /dev/null
+++ b/tests/diagnostics/cjs-require-tdz-unchecked/main.js
@@ -0,0 +1,8 @@
+// A const require binding read by a statement above the require that
+// lowers to no expression: publishing the binding as an export is module
+// plumbing, so no initialization check can run where Node throws
+// "Cannot access 'greet' before initialization". The read keeps SC1013.
+'use strict';
+
+module.exports = { greet };
+const { greet } = require('./lib.js');
diff --git a/tests/diagnostics/cjs-require-tdz-unchecked/package.json b/tests/diagnostics/cjs-require-tdz-unchecked/package.json
new file mode 100644
index 0000000000..5bbefffbab
--- /dev/null
+++ b/tests/diagnostics/cjs-require-tdz-unchecked/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "commonjs"
+}
diff --git a/tests/diagnostics/cjs-require-tdz/main.js b/tests/diagnostics/cjs-require-tdz/main.js
index a571b45242..c2b5a945ce 100644
--- a/tests/diagnostics/cjs-require-tdz/main.js
+++ b/tests/diagnostics/cjs-require-tdz/main.js
@@ -1,14 +1,15 @@
-// A require whose binding IS reachable from code above it: Node would
-// throw "Cannot access 'greet' before initialization" at the helper()
-// call (the const initializes AT the require), an ordering the compiled
-// aliasing does not model — the require keeps a named SC1013 fence.
+// A var require binding that code above the require can reach: Node reads
+// undefined there (the var is hoisted, the require has not run), a value
+// the compiled module alias cannot hold — the require keeps a named SC1013
+// fence. A const or let binding compiles with an initialization check
+// instead (tests/corpus/6141-cjs-require-tdz-checked).
'use strict';
console.log('boot', helper());
-const { greet } = require('./lib.js');
+var lib = require('./lib.js');
function helper() {
- return greet('early');
+ return lib.greet('early');
}
console.log(helper());
diff --git a/tests/fixtures/npm-static/cjs-interop-cli.ts b/tests/fixtures/npm-static/cjs-interop-cli.ts
new file mode 100644
index 0000000000..2b9a88f635
--- /dev/null
+++ b/tests/fixtures/npm-static/cjs-interop-cli.ts
@@ -0,0 +1,9 @@
+// npm-static pilots: real CommonJS packages that preflight refused before.
+// fs.realpath@1.0.0 and on-finished@2.4.1 assign module.exports and run
+// code before they require their dependencies (each binding is reachable
+// only after its require ran); statuses@2.0.1 requires its codes.json.
+import realpath from "fs.realpath";
+import onFinished from "on-finished";
+import status from "statuses";
+
+console.log(typeof realpath.realpathSync, typeof onFinished, typeof status);
diff --git a/tests/fixtures/npm-static/node_modules/ee-first/LICENSE b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE
new file mode 100644
index 0000000000..a7ae8ee9b8
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/ee-first/LICENSE
@@ -0,0 +1,22 @@
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Jonathan Ong me@jongleberry.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/tests/fixtures/npm-static/node_modules/ee-first/index.js b/tests/fixtures/npm-static/node_modules/ee-first/index.js
new file mode 100644
index 0000000000..501287cd3b
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/ee-first/index.js
@@ -0,0 +1,95 @@
+/*!
+ * ee-first
+ * Copyright(c) 2014 Jonathan Ong
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = first
+
+/**
+ * Get the first event in a set of event emitters and event pairs.
+ *
+ * @param {array} stuff
+ * @param {function} done
+ * @public
+ */
+
+function first(stuff, done) {
+ if (!Array.isArray(stuff))
+ throw new TypeError('arg must be an array of [ee, events...] arrays')
+
+ var cleanups = []
+
+ for (var i = 0; i < stuff.length; i++) {
+ var arr = stuff[i]
+
+ if (!Array.isArray(arr) || arr.length < 2)
+ throw new TypeError('each array member must be [ee, events...]')
+
+ var ee = arr[0]
+
+ for (var j = 1; j < arr.length; j++) {
+ var event = arr[j]
+ var fn = listener(event, callback)
+
+ // listen to the event
+ ee.on(event, fn)
+ // push this listener to the list of cleanups
+ cleanups.push({
+ ee: ee,
+ event: event,
+ fn: fn,
+ })
+ }
+ }
+
+ function callback() {
+ cleanup()
+ done.apply(null, arguments)
+ }
+
+ function cleanup() {
+ var x
+ for (var i = 0; i < cleanups.length; i++) {
+ x = cleanups[i]
+ x.ee.removeListener(x.event, x.fn)
+ }
+ }
+
+ function thunk(fn) {
+ done = fn
+ }
+
+ thunk.cancel = cleanup
+
+ return thunk
+}
+
+/**
+ * Create the event listener.
+ * @private
+ */
+
+function listener(event, done) {
+ return function onevent(arg1) {
+ var args = new Array(arguments.length)
+ var ee = this
+ var err = event === 'error'
+ ? arg1
+ : null
+
+ // copy args to prevent arguments escaping scope
+ for (var i = 0; i < args.length; i++) {
+ args[i] = arguments[i]
+ }
+
+ done(err, ee, event, args)
+ }
+}
diff --git a/tests/fixtures/npm-static/node_modules/ee-first/package.json b/tests/fixtures/npm-static/node_modules/ee-first/package.json
new file mode 100644
index 0000000000..b6d0b7d675
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/ee-first/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "ee-first",
+ "description": "return the first event in a set of ee/event pairs",
+ "version": "1.1.1",
+ "author": {
+ "name": "Jonathan Ong",
+ "email": "me@jongleberry.com",
+ "url": "http://jongleberry.com",
+ "twitter": "https://twitter.com/jongleberry"
+ },
+ "contributors": [
+ "Douglas Christopher Wilson "
+ ],
+ "license": "MIT",
+ "repository": "jonathanong/ee-first",
+ "devDependencies": {
+ "istanbul": "0.3.9",
+ "mocha": "2.2.5"
+ },
+ "files": [
+ "index.js",
+ "LICENSE"
+ ],
+ "scripts": {
+ "test": "mocha --reporter spec --bail --check-leaks test/",
+ "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/",
+ "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/"
+ }
+}
diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE
new file mode 100644
index 0000000000..5bd884c252
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/fs.realpath/LICENSE
@@ -0,0 +1,43 @@
+The ISC License
+
+Copyright (c) Isaac Z. Schlueter and Contributors
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
+IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+----
+
+This library bundles a version of the `fs.realpath` and `fs.realpathSync`
+methods from Node.js v0.10 under the terms of the Node.js MIT license.
+
+Node's license follows, also included at the header of `old.js` which contains
+the licensed code:
+
+ Copyright Joyent, Inc. and other Node contributors.
+
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/index.js b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js
new file mode 100644
index 0000000000..b09c7c7e63
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/fs.realpath/index.js
@@ -0,0 +1,66 @@
+module.exports = realpath
+realpath.realpath = realpath
+realpath.sync = realpathSync
+realpath.realpathSync = realpathSync
+realpath.monkeypatch = monkeypatch
+realpath.unmonkeypatch = unmonkeypatch
+
+var fs = require('fs')
+var origRealpath = fs.realpath
+var origRealpathSync = fs.realpathSync
+
+var version = process.version
+var ok = /^v[0-5]\./.test(version)
+var old = require('./old.js')
+
+function newError (er) {
+ return er && er.syscall === 'realpath' && (
+ er.code === 'ELOOP' ||
+ er.code === 'ENOMEM' ||
+ er.code === 'ENAMETOOLONG'
+ )
+}
+
+function realpath (p, cache, cb) {
+ if (ok) {
+ return origRealpath(p, cache, cb)
+ }
+
+ if (typeof cache === 'function') {
+ cb = cache
+ cache = null
+ }
+ origRealpath(p, cache, function (er, result) {
+ if (newError(er)) {
+ old.realpath(p, cache, cb)
+ } else {
+ cb(er, result)
+ }
+ })
+}
+
+function realpathSync (p, cache) {
+ if (ok) {
+ return origRealpathSync(p, cache)
+ }
+
+ try {
+ return origRealpathSync(p, cache)
+ } catch (er) {
+ if (newError(er)) {
+ return old.realpathSync(p, cache)
+ } else {
+ throw er
+ }
+ }
+}
+
+function monkeypatch () {
+ fs.realpath = realpath
+ fs.realpathSync = realpathSync
+}
+
+function unmonkeypatch () {
+ fs.realpath = origRealpath
+ fs.realpathSync = origRealpathSync
+}
diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/old.js b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js
new file mode 100644
index 0000000000..b40305e73f
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/fs.realpath/old.js
@@ -0,0 +1,303 @@
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+var pathModule = require('path');
+var isWindows = process.platform === 'win32';
+var fs = require('fs');
+
+// JavaScript implementation of realpath, ported from node pre-v6
+
+var DEBUG = process.env.NODE_DEBUG && /fs/.test(process.env.NODE_DEBUG);
+
+function rethrow() {
+ // Only enable in debug mode. A backtrace uses ~1000 bytes of heap space and
+ // is fairly slow to generate.
+ var callback;
+ if (DEBUG) {
+ var backtrace = new Error;
+ callback = debugCallback;
+ } else
+ callback = missingCallback;
+
+ return callback;
+
+ function debugCallback(err) {
+ if (err) {
+ backtrace.message = err.message;
+ err = backtrace;
+ missingCallback(err);
+ }
+ }
+
+ function missingCallback(err) {
+ if (err) {
+ if (process.throwDeprecation)
+ throw err; // Forgot a callback but don't know where? Use NODE_DEBUG=fs
+ else if (!process.noDeprecation) {
+ var msg = 'fs: missing callback ' + (err.stack || err.message);
+ if (process.traceDeprecation)
+ console.trace(msg);
+ else
+ console.error(msg);
+ }
+ }
+ }
+}
+
+function maybeCallback(cb) {
+ return typeof cb === 'function' ? cb : rethrow();
+}
+
+var normalize = pathModule.normalize;
+
+// Regexp that finds the next partion of a (partial) path
+// result is [base_with_slash, base], e.g. ['somedir/', 'somedir']
+if (isWindows) {
+ var nextPartRe = /(.*?)(?:[\/\\]+|$)/g;
+} else {
+ var nextPartRe = /(.*?)(?:[\/]+|$)/g;
+}
+
+// Regex to find the device root, including trailing slash. E.g. 'c:\\'.
+if (isWindows) {
+ var splitRootRe = /^(?:[a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?[\\\/]*/;
+} else {
+ var splitRootRe = /^[\/]*/;
+}
+
+exports.realpathSync = function realpathSync(p, cache) {
+ // make p is absolute
+ p = pathModule.resolve(p);
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
+ return cache[p];
+ }
+
+ var original = p,
+ seenLinks = {},
+ knownHard = {};
+
+ // current character position in p
+ var pos;
+ // the partial path so far, including a trailing slash if any
+ var current;
+ // the partial path without a trailing slash (except when pointing at a root)
+ var base;
+ // the partial path scanned in the previous round, with slash
+ var previous;
+
+ start();
+
+ function start() {
+ // Skip over roots
+ var m = splitRootRe.exec(p);
+ pos = m[0].length;
+ current = m[0];
+ base = m[0];
+ previous = '';
+
+ // On windows, check that the root exists. On unix there is no need.
+ if (isWindows && !knownHard[base]) {
+ fs.lstatSync(base);
+ knownHard[base] = true;
+ }
+ }
+
+ // walk down the path, swapping out linked pathparts for their real
+ // values
+ // NB: p.length changes.
+ while (pos < p.length) {
+ // find the next part
+ nextPartRe.lastIndex = pos;
+ var result = nextPartRe.exec(p);
+ previous = current;
+ current += result[0];
+ base = previous + result[1];
+ pos = nextPartRe.lastIndex;
+
+ // continue if not a symlink
+ if (knownHard[base] || (cache && cache[base] === base)) {
+ continue;
+ }
+
+ var resolvedLink;
+ if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
+ // some known symbolic link. no need to stat again.
+ resolvedLink = cache[base];
+ } else {
+ var stat = fs.lstatSync(base);
+ if (!stat.isSymbolicLink()) {
+ knownHard[base] = true;
+ if (cache) cache[base] = base;
+ continue;
+ }
+
+ // read the link if it wasn't read before
+ // dev/ino always return 0 on windows, so skip the check.
+ var linkTarget = null;
+ if (!isWindows) {
+ var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
+ if (seenLinks.hasOwnProperty(id)) {
+ linkTarget = seenLinks[id];
+ }
+ }
+ if (linkTarget === null) {
+ fs.statSync(base);
+ linkTarget = fs.readlinkSync(base);
+ }
+ resolvedLink = pathModule.resolve(previous, linkTarget);
+ // track this, if given a cache.
+ if (cache) cache[base] = resolvedLink;
+ if (!isWindows) seenLinks[id] = linkTarget;
+ }
+
+ // resolve the link, then start over
+ p = pathModule.resolve(resolvedLink, p.slice(pos));
+ start();
+ }
+
+ if (cache) cache[original] = p;
+
+ return p;
+};
+
+
+exports.realpath = function realpath(p, cache, cb) {
+ if (typeof cb !== 'function') {
+ cb = maybeCallback(cache);
+ cache = null;
+ }
+
+ // make p is absolute
+ p = pathModule.resolve(p);
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, p)) {
+ return process.nextTick(cb.bind(null, null, cache[p]));
+ }
+
+ var original = p,
+ seenLinks = {},
+ knownHard = {};
+
+ // current character position in p
+ var pos;
+ // the partial path so far, including a trailing slash if any
+ var current;
+ // the partial path without a trailing slash (except when pointing at a root)
+ var base;
+ // the partial path scanned in the previous round, with slash
+ var previous;
+
+ start();
+
+ function start() {
+ // Skip over roots
+ var m = splitRootRe.exec(p);
+ pos = m[0].length;
+ current = m[0];
+ base = m[0];
+ previous = '';
+
+ // On windows, check that the root exists. On unix there is no need.
+ if (isWindows && !knownHard[base]) {
+ fs.lstat(base, function(err) {
+ if (err) return cb(err);
+ knownHard[base] = true;
+ LOOP();
+ });
+ } else {
+ process.nextTick(LOOP);
+ }
+ }
+
+ // walk down the path, swapping out linked pathparts for their real
+ // values
+ function LOOP() {
+ // stop if scanned past end of path
+ if (pos >= p.length) {
+ if (cache) cache[original] = p;
+ return cb(null, p);
+ }
+
+ // find the next part
+ nextPartRe.lastIndex = pos;
+ var result = nextPartRe.exec(p);
+ previous = current;
+ current += result[0];
+ base = previous + result[1];
+ pos = nextPartRe.lastIndex;
+
+ // continue if not a symlink
+ if (knownHard[base] || (cache && cache[base] === base)) {
+ return process.nextTick(LOOP);
+ }
+
+ if (cache && Object.prototype.hasOwnProperty.call(cache, base)) {
+ // known symbolic link. no need to stat again.
+ return gotResolvedLink(cache[base]);
+ }
+
+ return fs.lstat(base, gotStat);
+ }
+
+ function gotStat(err, stat) {
+ if (err) return cb(err);
+
+ // if not a symlink, skip to the next path part
+ if (!stat.isSymbolicLink()) {
+ knownHard[base] = true;
+ if (cache) cache[base] = base;
+ return process.nextTick(LOOP);
+ }
+
+ // stat & read the link if not read before
+ // call gotTarget as soon as the link target is known
+ // dev/ino always return 0 on windows, so skip the check.
+ if (!isWindows) {
+ var id = stat.dev.toString(32) + ':' + stat.ino.toString(32);
+ if (seenLinks.hasOwnProperty(id)) {
+ return gotTarget(null, seenLinks[id], base);
+ }
+ }
+ fs.stat(base, function(err) {
+ if (err) return cb(err);
+
+ fs.readlink(base, function(err, target) {
+ if (!isWindows) seenLinks[id] = target;
+ gotTarget(err, target);
+ });
+ });
+ }
+
+ function gotTarget(err, target, base) {
+ if (err) return cb(err);
+
+ var resolvedLink = pathModule.resolve(previous, target);
+ if (cache) cache[base] = resolvedLink;
+ gotResolvedLink(resolvedLink);
+ }
+
+ function gotResolvedLink(resolvedLink) {
+ // resolve the link, then start over
+ p = pathModule.resolve(resolvedLink, p.slice(pos));
+ start();
+ }
+};
diff --git a/tests/fixtures/npm-static/node_modules/fs.realpath/package.json b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json
new file mode 100644
index 0000000000..3edc57d21c
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/fs.realpath/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "fs.realpath",
+ "version": "1.0.0",
+ "description": "Use node's fs.realpath, but fall back to the JS implementation if the native one fails",
+ "main": "index.js",
+ "dependencies": {},
+ "devDependencies": {},
+ "scripts": {
+ "test": "tap test/*.js --cov"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/isaacs/fs.realpath.git"
+ },
+ "keywords": [
+ "realpath",
+ "fs",
+ "polyfill"
+ ],
+ "author": "Isaac Z. Schlueter (http://blog.izs.me/)",
+ "license": "ISC",
+ "files": [
+ "old.js",
+ "index.js"
+ ]
+}
diff --git a/tests/fixtures/npm-static/node_modules/on-finished/LICENSE b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE
new file mode 100644
index 0000000000..5931fd23ea
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/on-finished/LICENSE
@@ -0,0 +1,23 @@
+(The MIT License)
+
+Copyright (c) 2013 Jonathan Ong
+Copyright (c) 2014 Douglas Christopher Wilson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/tests/fixtures/npm-static/node_modules/on-finished/index.js b/tests/fixtures/npm-static/node_modules/on-finished/index.js
new file mode 100644
index 0000000000..e68df7bde3
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/on-finished/index.js
@@ -0,0 +1,234 @@
+/*!
+ * on-finished
+ * Copyright(c) 2013 Jonathan Ong
+ * Copyright(c) 2014 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = onFinished
+module.exports.isFinished = isFinished
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var asyncHooks = tryRequireAsyncHooks()
+var first = require('ee-first')
+
+/**
+ * Variables.
+ * @private
+ */
+
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
+ ? setImmediate
+ : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
+
+/**
+ * Invoke callback when the response has finished, useful for
+ * cleaning up resources afterwards.
+ *
+ * @param {object} msg
+ * @param {function} listener
+ * @return {object}
+ * @public
+ */
+
+function onFinished (msg, listener) {
+ if (isFinished(msg) !== false) {
+ defer(listener, null, msg)
+ return msg
+ }
+
+ // attach the listener to the message
+ attachListener(msg, wrap(listener))
+
+ return msg
+}
+
+/**
+ * Determine if message is already finished.
+ *
+ * @param {object} msg
+ * @return {boolean}
+ * @public
+ */
+
+function isFinished (msg) {
+ var socket = msg.socket
+
+ if (typeof msg.finished === 'boolean') {
+ // OutgoingMessage
+ return Boolean(msg.finished || (socket && !socket.writable))
+ }
+
+ if (typeof msg.complete === 'boolean') {
+ // IncomingMessage
+ return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable))
+ }
+
+ // don't know
+ return undefined
+}
+
+/**
+ * Attach a finished listener to the message.
+ *
+ * @param {object} msg
+ * @param {function} callback
+ * @private
+ */
+
+function attachFinishedListener (msg, callback) {
+ var eeMsg
+ var eeSocket
+ var finished = false
+
+ function onFinish (error) {
+ eeMsg.cancel()
+ eeSocket.cancel()
+
+ finished = true
+ callback(error)
+ }
+
+ // finished on first message event
+ eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish)
+
+ function onSocket (socket) {
+ // remove listener
+ msg.removeListener('socket', onSocket)
+
+ if (finished) return
+ if (eeMsg !== eeSocket) return
+
+ // finished on first socket event
+ eeSocket = first([[socket, 'error', 'close']], onFinish)
+ }
+
+ if (msg.socket) {
+ // socket already assigned
+ onSocket(msg.socket)
+ return
+ }
+
+ // wait for socket to be assigned
+ msg.on('socket', onSocket)
+
+ if (msg.socket === undefined) {
+ // istanbul ignore next: node.js 0.8 patch
+ patchAssignSocket(msg, onSocket)
+ }
+}
+
+/**
+ * Attach the listener to the message.
+ *
+ * @param {object} msg
+ * @return {function}
+ * @private
+ */
+
+function attachListener (msg, listener) {
+ var attached = msg.__onFinished
+
+ // create a private single listener with queue
+ if (!attached || !attached.queue) {
+ attached = msg.__onFinished = createListener(msg)
+ attachFinishedListener(msg, attached)
+ }
+
+ attached.queue.push(listener)
+}
+
+/**
+ * Create listener on message.
+ *
+ * @param {object} msg
+ * @return {function}
+ * @private
+ */
+
+function createListener (msg) {
+ function listener (err) {
+ if (msg.__onFinished === listener) msg.__onFinished = null
+ if (!listener.queue) return
+
+ var queue = listener.queue
+ listener.queue = null
+
+ for (var i = 0; i < queue.length; i++) {
+ queue[i](err, msg)
+ }
+ }
+
+ listener.queue = []
+
+ return listener
+}
+
+/**
+ * Patch ServerResponse.prototype.assignSocket for node.js 0.8.
+ *
+ * @param {ServerResponse} res
+ * @param {function} callback
+ * @private
+ */
+
+// istanbul ignore next: node.js 0.8 patch
+function patchAssignSocket (res, callback) {
+ var assignSocket = res.assignSocket
+
+ if (typeof assignSocket !== 'function') return
+
+ // res.on('socket', callback) is broken in 0.8
+ res.assignSocket = function _assignSocket (socket) {
+ assignSocket.call(this, socket)
+ callback(socket)
+ }
+}
+
+/**
+ * Try to require async_hooks
+ * @private
+ */
+
+function tryRequireAsyncHooks () {
+ try {
+ return require('async_hooks')
+ } catch (e) {
+ return {}
+ }
+}
+
+/**
+ * Wrap function with async resource, if possible.
+ * AsyncResource.bind static method backported.
+ * @private
+ */
+
+function wrap (fn) {
+ var res
+
+ // create anonymous resource
+ if (asyncHooks.AsyncResource) {
+ res = new asyncHooks.AsyncResource(fn.name || 'bound-anonymous-fn')
+ }
+
+ // incompatible node.js
+ if (!res || !res.runInAsyncScope) {
+ return fn
+ }
+
+ // return bound function
+ return res.runInAsyncScope.bind(res, fn, null)
+}
diff --git a/tests/fixtures/npm-static/node_modules/on-finished/package.json b/tests/fixtures/npm-static/node_modules/on-finished/package.json
new file mode 100644
index 0000000000..644cd814b9
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/on-finished/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "on-finished",
+ "description": "Execute a callback when a request closes, finishes, or errors",
+ "version": "2.4.1",
+ "contributors": [
+ "Douglas Christopher Wilson ",
+ "Jonathan Ong (http://jongleberry.com)"
+ ],
+ "license": "MIT",
+ "repository": "jshttp/on-finished",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "devDependencies": {
+ "eslint": "7.32.0",
+ "eslint-config-standard": "14.1.1",
+ "eslint-plugin-import": "2.25.4",
+ "eslint-plugin-markdown": "2.2.1",
+ "eslint-plugin-node": "11.1.0",
+ "eslint-plugin-promise": "5.2.0",
+ "eslint-plugin-standard": "4.1.0",
+ "mocha": "9.2.1",
+ "nyc": "15.1.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "files": [
+ "HISTORY.md",
+ "LICENSE",
+ "index.js"
+ ],
+ "scripts": {
+ "lint": "eslint .",
+ "test": "mocha --reporter spec --bail --check-leaks test/",
+ "test-ci": "nyc --reporter=lcovonly --reporter=text npm test",
+ "test-cov": "nyc --reporter=html --reporter=text npm test"
+ }
+}
diff --git a/tests/fixtures/npm-static/node_modules/statuses/LICENSE b/tests/fixtures/npm-static/node_modules/statuses/LICENSE
new file mode 100644
index 0000000000..28a3161820
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/statuses/LICENSE
@@ -0,0 +1,23 @@
+
+The MIT License (MIT)
+
+Copyright (c) 2014 Jonathan Ong
+Copyright (c) 2016 Douglas Christopher Wilson
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/tests/fixtures/npm-static/node_modules/statuses/codes.json b/tests/fixtures/npm-static/node_modules/statuses/codes.json
new file mode 100644
index 0000000000..1333ed10ba
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/statuses/codes.json
@@ -0,0 +1,65 @@
+{
+ "100": "Continue",
+ "101": "Switching Protocols",
+ "102": "Processing",
+ "103": "Early Hints",
+ "200": "OK",
+ "201": "Created",
+ "202": "Accepted",
+ "203": "Non-Authoritative Information",
+ "204": "No Content",
+ "205": "Reset Content",
+ "206": "Partial Content",
+ "207": "Multi-Status",
+ "208": "Already Reported",
+ "226": "IM Used",
+ "300": "Multiple Choices",
+ "301": "Moved Permanently",
+ "302": "Found",
+ "303": "See Other",
+ "304": "Not Modified",
+ "305": "Use Proxy",
+ "307": "Temporary Redirect",
+ "308": "Permanent Redirect",
+ "400": "Bad Request",
+ "401": "Unauthorized",
+ "402": "Payment Required",
+ "403": "Forbidden",
+ "404": "Not Found",
+ "405": "Method Not Allowed",
+ "406": "Not Acceptable",
+ "407": "Proxy Authentication Required",
+ "408": "Request Timeout",
+ "409": "Conflict",
+ "410": "Gone",
+ "411": "Length Required",
+ "412": "Precondition Failed",
+ "413": "Payload Too Large",
+ "414": "URI Too Long",
+ "415": "Unsupported Media Type",
+ "416": "Range Not Satisfiable",
+ "417": "Expectation Failed",
+ "418": "I'm a Teapot",
+ "421": "Misdirected Request",
+ "422": "Unprocessable Entity",
+ "423": "Locked",
+ "424": "Failed Dependency",
+ "425": "Too Early",
+ "426": "Upgrade Required",
+ "428": "Precondition Required",
+ "429": "Too Many Requests",
+ "431": "Request Header Fields Too Large",
+ "451": "Unavailable For Legal Reasons",
+ "500": "Internal Server Error",
+ "501": "Not Implemented",
+ "502": "Bad Gateway",
+ "503": "Service Unavailable",
+ "504": "Gateway Timeout",
+ "505": "HTTP Version Not Supported",
+ "506": "Variant Also Negotiates",
+ "507": "Insufficient Storage",
+ "508": "Loop Detected",
+ "509": "Bandwidth Limit Exceeded",
+ "510": "Not Extended",
+ "511": "Network Authentication Required"
+}
diff --git a/tests/fixtures/npm-static/node_modules/statuses/index.js b/tests/fixtures/npm-static/node_modules/statuses/index.js
new file mode 100644
index 0000000000..ea351c5535
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/statuses/index.js
@@ -0,0 +1,146 @@
+/*!
+ * statuses
+ * Copyright(c) 2014 Jonathan Ong
+ * Copyright(c) 2016 Douglas Christopher Wilson
+ * MIT Licensed
+ */
+
+'use strict'
+
+/**
+ * Module dependencies.
+ * @private
+ */
+
+var codes = require('./codes.json')
+
+/**
+ * Module exports.
+ * @public
+ */
+
+module.exports = status
+
+// status code to message map
+status.message = codes
+
+// status message (lower-case) to code map
+status.code = createMessageToStatusCodeMap(codes)
+
+// array of status codes
+status.codes = createStatusCodeList(codes)
+
+// status codes for redirects
+status.redirect = {
+ 300: true,
+ 301: true,
+ 302: true,
+ 303: true,
+ 305: true,
+ 307: true,
+ 308: true
+}
+
+// status codes for empty bodies
+status.empty = {
+ 204: true,
+ 205: true,
+ 304: true
+}
+
+// status codes for when you should retry the request
+status.retry = {
+ 502: true,
+ 503: true,
+ 504: true
+}
+
+/**
+ * Create a map of message to status code.
+ * @private
+ */
+
+function createMessageToStatusCodeMap (codes) {
+ var map = {}
+
+ Object.keys(codes).forEach(function forEachCode (code) {
+ var message = codes[code]
+ var status = Number(code)
+
+ // populate map
+ map[message.toLowerCase()] = status
+ })
+
+ return map
+}
+
+/**
+ * Create a list of all status codes.
+ * @private
+ */
+
+function createStatusCodeList (codes) {
+ return Object.keys(codes).map(function mapCode (code) {
+ return Number(code)
+ })
+}
+
+/**
+ * Get the status code for given message.
+ * @private
+ */
+
+function getStatusCode (message) {
+ var msg = message.toLowerCase()
+
+ if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
+ throw new Error('invalid status message: "' + message + '"')
+ }
+
+ return status.code[msg]
+}
+
+/**
+ * Get the status message for given code.
+ * @private
+ */
+
+function getStatusMessage (code) {
+ if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
+ throw new Error('invalid status code: ' + code)
+ }
+
+ return status.message[code]
+}
+
+/**
+ * Get the status code.
+ *
+ * Given a number, this will throw if it is not a known status
+ * code, otherwise the code will be returned. Given a string,
+ * the string will be parsed for a number and return the code
+ * if valid, otherwise will lookup the code assuming this is
+ * the status message.
+ *
+ * @param {string|number} code
+ * @returns {number}
+ * @public
+ */
+
+function status (code) {
+ if (typeof code === 'number') {
+ return getStatusMessage(code)
+ }
+
+ if (typeof code !== 'string') {
+ throw new TypeError('code must be a number or string')
+ }
+
+ // '403'
+ var n = parseInt(code, 10)
+ if (!isNaN(n)) {
+ return getStatusMessage(n)
+ }
+
+ return getStatusCode(code)
+}
diff --git a/tests/fixtures/npm-static/node_modules/statuses/package.json b/tests/fixtures/npm-static/node_modules/statuses/package.json
new file mode 100644
index 0000000000..8c3e719b50
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/statuses/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "statuses",
+ "description": "HTTP status utility",
+ "version": "2.0.1",
+ "contributors": [
+ "Douglas Christopher Wilson ",
+ "Jonathan Ong (http://jongleberry.com)"
+ ],
+ "repository": "jshttp/statuses",
+ "license": "MIT",
+ "keywords": [
+ "http",
+ "status",
+ "code"
+ ],
+ "files": [
+ "HISTORY.md",
+ "index.js",
+ "codes.json",
+ "LICENSE"
+ ],
+ "devDependencies": {
+ "csv-parse": "4.14.2",
+ "eslint": "7.17.0",
+ "eslint-config-standard": "14.1.1",
+ "eslint-plugin-import": "2.22.1",
+ "eslint-plugin-markdown": "1.0.2",
+ "eslint-plugin-node": "11.1.0",
+ "eslint-plugin-promise": "4.2.1",
+ "eslint-plugin-standard": "4.1.0",
+ "mocha": "8.2.1",
+ "nyc": "15.1.0",
+ "raw-body": "2.4.1",
+ "stream-to-array": "2.3.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "scripts": {
+ "build": "node scripts/build.js",
+ "fetch": "node scripts/fetch-apache.js && node scripts/fetch-iana.js && node scripts/fetch-nginx.js && node scripts/fetch-node.js",
+ "lint": "eslint --plugin markdown --ext js,md .",
+ "test": "mocha --reporter spec --check-leaks --bail test/",
+ "test-ci": "nyc --reporter=lcov --reporter=text npm test",
+ "test-cov": "nyc --reporter=html --reporter=text npm test",
+ "update": "npm run fetch && npm run build",
+ "version": "node scripts/version-history.js && git add HISTORY.md"
+ }
+}
diff --git a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt
index ad55be5244..8916284c3a 100644
--- a/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt
+++ b/tests/harness/__snapshots__/cjs-require-cycle/main.js.txt
@@ -1,4 +1,4 @@
-cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet
+cjs-require-cycle/a.js:1:1 - error SC1016: circular imports (cjs-require-cycle/a.js → cjs-require-cycle/b.js → cjs-require-cycle/a.js; the cycle closes through a require() edge: cjs-require-cycle/a.js uses its exports at cjs-require-cycle/a.js:6 outside the export statements that precede its first require, so a module of the cycle can observe them before they settle) are not supported yet
1 | 'use strict';
| ^
diff --git a/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt
new file mode 100644
index 0000000000..eda869e9a6
--- /dev/null
+++ b/tests/harness/__snapshots__/cjs-require-tdz-unchecked/main.js.txt
@@ -0,0 +1,6 @@
+cjs-require-tdz-unchecked/main.js:7:20 - error SC1013: reading require() binding 'greet' where it can run before the require (this use has no checked form for Node's ReferenceError — move the require above the code that can run first) is not supported yet
+
+ 6 |
+ 7 | module.exports = { greet };
+ | ^~~~~
+ 8 | const { greet } = require('./lib.js');
\ No newline at end of file
diff --git a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt
index 6316ce98b4..295f7cd0c1 100644
--- a/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt
+++ b/tests/harness/__snapshots__/cjs-require-tdz/main.js.txt
@@ -1,6 +1,6 @@
-cjs-require-tdz/main.js:8:7 - error SC1013: require() below code that can reach its binding 'greet' (Node initializes the binding AT the require and throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first) is not supported yet
+cjs-require-tdz/main.js:9:5 - error SC1013: var require() binding 'lib' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first) is not supported yet
- 7 | console.log('boot', helper());
- 8 | const { greet } = require('./lib.js');
- | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- 9 |
\ No newline at end of file
+ 8 | console.log('boot', helper());
+ 9 | var lib = require('./lib.js');
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~
+ 10 |
\ No newline at end of file
diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts
index 89bd2e0b8d..c5021fd062 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"), {