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"), {