mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Compile classic CommonJS packages: late requires, JSON requires, nested builtin destructuring, settled require cycles (#1287)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -32,6 +32,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/language-features/dynamic-imports.mdx</include>
|
||||
|
||||
<include>../limitations/language-features/commonjs-require.mdx</include>
|
||||
|
||||
### Types and class definitions
|
||||
|
||||
<include>../limitations/types-and-class-definitions/bigint.mdx</include>
|
||||
|
||||
@@ -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`.
|
||||
@@ -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)
|
||||
|
||||
@@ -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<JsonRequireModule>();
|
||||
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) };
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
|
||||
* explicit resource management, and blocked-binding poisoning. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
@@ -1512,81 +1514,96 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement):
|
||||
const isLet =
|
||||
(list.flags & ts.NodeFlags.Let) !== 0 || (list.flags & ts.NodeFlags.BlockScoped) === 0;
|
||||
return list.declarations.flatMap((decl) => {
|
||||
// The canonical ESM bridge to a compiled program module. Like a
|
||||
// top-level CommonJS require declaration, the binding is alias
|
||||
// plumbing while the dependency evaluates at this statement's
|
||||
// exact position through its run-once initializer.
|
||||
if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) {
|
||||
if (!ts.isSourceFile(stmt.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
decl,
|
||||
"createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)",
|
||||
);
|
||||
}
|
||||
const target = createRequireProgramModuleOf(lowerer, decl.initializer)!;
|
||||
const init = lowerer.requireInitStmt(target.spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
// CommonJS require declarations (JS files): at the module's top
|
||||
// level the BINDINGS are alias plumbing (no storage;
|
||||
// resolveValueSymbol routes the reads), but the require itself is
|
||||
// Node's inline module evaluation — a relative require lowers to the
|
||||
// required module's guarded %init call at exactly this position
|
||||
// (first require runs the body, later ones are cache hits; builtins
|
||||
// load nothing). Binding requires anywhere else would need lazily-
|
||||
// initialized alias storage this model doesn't represent; named
|
||||
// fence.
|
||||
if (
|
||||
decl.initializer !== undefined &&
|
||||
requireSpecOf(decl.initializer) !== null &&
|
||||
!isNativeFfiRequire(lowerer, decl.initializer) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
) {
|
||||
const spec = requireSpecOf(decl.initializer)!;
|
||||
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
|
||||
if (ts.isSourceFile(stmt.parent)) {
|
||||
const init = lowerer.requireInitStmt(spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
// A nested BUILTIN require (`get inFreeBSDJail() { const { execSync }
|
||||
// = require('child_process'); ... }` — test/common's lazy-getter
|
||||
// idiom): builtins load nothing, so the statement is PURE alias
|
||||
// plumbing wherever it sits — the bindings resolve through the
|
||||
// same routing top-level requires use, and no statement remains.
|
||||
// Relative requires keep the fence: their module-init-at-position
|
||||
// semantics needs storage this model doesn't represent.
|
||||
if (canonicalBuiltinModule(spec) !== null) {
|
||||
return [];
|
||||
}
|
||||
const lowered = lowerVarDeclarator(lowerer, stmt, decl, isConst, isLet);
|
||||
// A require binding earlier code can read is initialized from here on
|
||||
// (require-initialization.ts).
|
||||
const initialized = requireInitFlagStmt(lowerer, decl);
|
||||
return initialized ? [...lowered, initialized] : lowered;
|
||||
});
|
||||
}
|
||||
|
||||
function lowerVarDeclarator(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.VariableStatement,
|
||||
decl: ts.VariableDeclaration,
|
||||
isConst: boolean,
|
||||
isLet: boolean,
|
||||
): IrStmt[] {
|
||||
// The canonical ESM bridge to a compiled program module. Like a
|
||||
// top-level CommonJS require declaration, the binding is alias
|
||||
// plumbing while the dependency evaluates at this statement's
|
||||
// exact position through its run-once initializer.
|
||||
if (isConst && createRequireProgramModuleDecl(lowerer, decl.name, decl.initializer)) {
|
||||
if (!ts.isSourceFile(stmt.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
decl,
|
||||
"require() with bindings outside the module's top level (move it to the top of the file)",
|
||||
"createRequire program-module bindings outside the module's top level (move the binding to the top of the file; side-effect-only requires may remain nested)",
|
||||
);
|
||||
}
|
||||
// `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over
|
||||
// the baked constants table: alias plumbing, no storage (uses read
|
||||
// their literals via builtinConstantBindingOf; collectGlobals skipped
|
||||
// the globals by the same test).
|
||||
if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return [];
|
||||
if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
|
||||
// `const { createSign } = crypto` over a builtin NAMESPACE binding:
|
||||
// alias plumbing (builtinImportOf routes the reads) — no statement.
|
||||
if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return [];
|
||||
return lowerer.lowerDestructuringDecl(decl, isLet);
|
||||
const target = createRequireProgramModuleOf(lowerer, decl.initializer)!;
|
||||
const init = lowerer.requireInitStmt(target.spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
// CommonJS require declarations (JS files): at the module's top
|
||||
// level the BINDINGS are alias plumbing (no storage;
|
||||
// resolveValueSymbol routes the reads), but the require itself is
|
||||
// Node's inline module evaluation — a relative require lowers to the
|
||||
// required module's guarded %init call at exactly this position
|
||||
// (first require runs the body, later ones are cache hits; builtins
|
||||
// load nothing). Binding requires anywhere else would need lazily-
|
||||
// initialized alias storage this model doesn't represent; named
|
||||
// fence.
|
||||
if (
|
||||
decl.initializer !== undefined &&
|
||||
requireSpecOf(decl.initializer) !== null &&
|
||||
!isNativeFfiRequire(lowerer, decl.initializer) &&
|
||||
!isJsonRequireDecl(lowerer, decl) &&
|
||||
isJsSourceFile(decl.getSourceFile())
|
||||
) {
|
||||
const spec = requireSpecOf(decl.initializer)!;
|
||||
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
|
||||
if (ts.isSourceFile(stmt.parent)) {
|
||||
const init = lowerer.requireInitStmt(spec, decl);
|
||||
return init ? [init] : [];
|
||||
}
|
||||
// STORED protocol values with statically closed representations:
|
||||
// numeric value iterators retain source/cursor/done hidden locals; a
|
||||
// matchAll drain retains its companion index array so a later
|
||||
// for-of can serve `m.index`. Both are const-only interceptions.
|
||||
const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst);
|
||||
if (numericIterator) return numericIterator;
|
||||
const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst);
|
||||
if (drained) return drained;
|
||||
const lowered = lowerer.lowerVarDecl(decl, isLet);
|
||||
return lowered ? [lowered] : [];
|
||||
});
|
||||
// A nested BUILTIN require (`get inFreeBSDJail() { const { execSync }
|
||||
// = require('child_process'); ... }` — test/common's lazy-getter
|
||||
// idiom): builtins load nothing, so the statement is PURE alias
|
||||
// plumbing wherever it sits — the bindings resolve through the
|
||||
// same routing top-level requires use, and no statement remains.
|
||||
// Relative requires keep the fence: their module-init-at-position
|
||||
// semantics needs storage this model doesn't represent.
|
||||
if (canonicalBuiltinModule(spec) !== null) {
|
||||
return [];
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
decl,
|
||||
"require() with bindings outside the module's top level (move it to the top of the file)",
|
||||
);
|
||||
}
|
||||
// `const { NGHTTP2_CANCEL } = http2.constants` — a destructure over
|
||||
// the baked constants table: alias plumbing, no storage (uses read
|
||||
// their literals via builtinConstantBindingOf; collectGlobals skipped
|
||||
// the globals by the same test).
|
||||
if (lowerer.builtinConstantsDestructureDecl(decl.name, decl.initializer)) return [];
|
||||
if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
|
||||
// `const { createSign } = crypto` over a builtin NAMESPACE binding:
|
||||
// alias plumbing (builtinImportOf routes the reads) — no statement.
|
||||
if (builtinNamespaceDestructureModuleOf(lowerer, decl) !== null) return [];
|
||||
return lowerer.lowerDestructuringDecl(decl, isLet);
|
||||
}
|
||||
// STORED protocol values with statically closed representations:
|
||||
// numeric value iterators retain source/cursor/done hidden locals; a
|
||||
// matchAll drain retains its companion index array so a later
|
||||
// for-of can serve `m.index`. Both are const-only interceptions.
|
||||
const numericIterator = lowerNumericIteratorDecl(lowerer, decl, isConst);
|
||||
if (numericIterator) return numericIterator;
|
||||
const drained = lowerMatchAllDrainDecl(lowerer, decl, isConst);
|
||||
if (drained) return drained;
|
||||
const lowered = lowerer.lowerVarDecl(decl, isLet);
|
||||
return lowered ? [lowered] : [];
|
||||
}
|
||||
|
||||
/** The numeric indexed source of a built-in value-iterator expression:
|
||||
|
||||
@@ -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<ts.SourceFile, IrStmt[]>();
|
||||
/** CommonJS require() of JSON documents: the shared object per document
|
||||
* and the calls reading it (json-require.ts). */
|
||||
readonly jsonRequireModules = new Map<string, JsonRequireModule>();
|
||||
readonly jsonRequireCalls = new Map<ts.CallExpression, JsonRequireModule>();
|
||||
/** 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<string, CycleInitFlag>();
|
||||
readonly cycleInitDeclarations = new Map<ts.Statement, CycleInitFlag[]>();
|
||||
/** CommonJS require declarations whose bindings code running before the
|
||||
* require can read, and those references (require-initialization.ts). */
|
||||
readonly requireInitFlags = new Map<ts.VariableDeclaration, RequireInitFlag>();
|
||||
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
|
||||
|
||||
@@ -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<ts.Symbol, string>;
|
||||
}
|
||||
|
||||
/** 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<ts.Symbol, { flag: RequireInitFlag; decl: ts.VariableDeclaration }>();
|
||||
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<string>();
|
||||
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<string, EarlyReference[]>();
|
||||
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<EarlyReference>();
|
||||
const loweredBodies = new Set<EarlyReference>();
|
||||
const claimed = new Set<EarlyReference>();
|
||||
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<string>();
|
||||
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<string>();
|
||||
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)`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SourceFile>();
|
||||
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<VariableDeclaration, Identifier | null>();
|
||||
/** Per file: the first top-level statement that can run user code. */
|
||||
readonly requireFirstRunnable = new Map<SourceFile, number>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
`
|
||||
|
||||
@@ -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<ts.Symbol>();
|
||||
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<ts.Symbol, ts.Node[]>();
|
||||
const stored = new Set<ts.Node>();
|
||||
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<string, ts.Node[]>();
|
||||
const ownerLinks = new Map<ts.Symbol, ts.Node[]>();
|
||||
const reachedKeys = new Set<string>();
|
||||
const writtenKeys = new Set<string>();
|
||||
const reachedUnits = new Set<ts.Node>();
|
||||
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<ts.SourceFile[], string | null>();
|
||||
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
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6140-cjs-require-tdz-throws/lib.js",
|
||||
"<repo>/tests/corpus/6140-cjs-require-tdz-throws/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6141-cjs-require-tdz-checked/lib.js",
|
||||
"<repo>/tests/corpus/6141-cjs-require-tdz-checked/counter.js",
|
||||
"<repo>/tests/corpus/6141-cjs-require-tdz-checked/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/parse-options.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/debug.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/comparator.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/helpers.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/glob.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/core.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/bundle.js",
|
||||
"<repo>/tests/corpus/6142-cjs-require-published-early/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6143-cjs-require-json/counter.js",
|
||||
"<repo>/tests/corpus/6143-cjs-require-json/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6144-cjs-require-builtin-nested/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6145-cjs-require-cycle/range.js",
|
||||
"<repo>/tests/corpus/6145-cjs-require-cycle/util.js",
|
||||
"<repo>/tests/corpus/6145-cjs-require-cycle/comparator.js",
|
||||
"<repo>/tests/corpus/6145-cjs-require-cycle/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC1016",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cjs-require-cycle/a.js → <repo>/tests/diagnostics/cjs-require-cycle/b.js → <repo>/tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge) are not supported yet",
|
||||
"message": "circular imports (<repo>/tests/diagnostics/cjs-require-cycle/a.js → <repo>/tests/diagnostics/cjs-require-cycle/b.js → <repo>/tests/diagnostics/cjs-require-cycle/a.js; the cycle closes through a require() edge: <repo>/tests/diagnostics/cjs-require-cycle/a.js uses its exports at <repo>/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": "<repo>/tests/diagnostics/cjs-require-cycle/a.js",
|
||||
"start": 0,
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/cjs-require-tdz-unchecked/lib.js",
|
||||
"<repo>/tests/diagnostics/cjs-require-tdz-unchecked/main.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+3
-3
@@ -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": "<repo>/tests/diagnostics/cjs-require-tdz/main.js",
|
||||
"start": 336,
|
||||
"end": 367
|
||||
"start": 402,
|
||||
"end": 427
|
||||
},
|
||||
"milestone": "later"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/** @param {string} who */
|
||||
function greet(who) {
|
||||
return `hello ${who}`;
|
||||
}
|
||||
|
||||
module.exports = { greet };
|
||||
@@ -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());
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -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;
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/** @param {string} who */
|
||||
function greet(who) {
|
||||
return `hello ${who}`;
|
||||
}
|
||||
|
||||
module.exports = { greet, answer: 42 };
|
||||
@@ -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());
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -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');
|
||||
@@ -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');
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
console.log('core: init');
|
||||
|
||||
/** @param {string} name */
|
||||
function label(name) {
|
||||
return `<${name}>`;
|
||||
}
|
||||
|
||||
module.exports = { label };
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
console.log('debug: init');
|
||||
|
||||
/** @param {...unknown} args */
|
||||
function debug(...args) {
|
||||
console.log('debug:', args.join(' '));
|
||||
}
|
||||
|
||||
module.exports = { debug };
|
||||
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
console.log('helpers: init');
|
||||
|
||||
/** @param {string} s */
|
||||
function normalize(s) {
|
||||
return s.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
module.exports = { normalize };
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/** @param {{ loose?: boolean } | undefined} options */
|
||||
function parseOptions(options) {
|
||||
return options && options.loose ? 'loose' : 'strict';
|
||||
}
|
||||
|
||||
module.exports = parseOptions;
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "fixture-data",
|
||||
"version": "1.2.3",
|
||||
"count": 41,
|
||||
"tags": ["native", "json"],
|
||||
"nested": { "flag": true, "ratio": 0.5 }
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
[3, 1, 2]
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -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')));
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -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 '));
|
||||
@@ -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));
|
||||
@@ -0,0 +1 @@
|
||||
{ "type": "commonjs" }
|
||||
@@ -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');
|
||||
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
console.log('util: loaded');
|
||||
|
||||
/** @param {string} s */
|
||||
function trim(s) {
|
||||
return s.trim();
|
||||
}
|
||||
|
||||
module.exports = { trim };
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
/** @param {string} who */
|
||||
function greet(who) {
|
||||
return `hello ${who}`;
|
||||
}
|
||||
|
||||
module.exports = { greet };
|
||||
@@ -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');
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
+22
@@ -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.
|
||||
+95
@@ -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)
|
||||
}
|
||||
}
|
||||
+29
@@ -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 <doug@somethingdoug.com>"
|
||||
],
|
||||
"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/"
|
||||
}
|
||||
}
|
||||
+43
@@ -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.
|
||||
+66
@@ -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
|
||||
}
|
||||
+303
@@ -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();
|
||||
}
|
||||
};
|
||||
+26
@@ -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 <i@izs.me> (http://blog.izs.me/)",
|
||||
"license": "ISC",
|
||||
"files": [
|
||||
"old.js",
|
||||
"index.js"
|
||||
]
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2014 Douglas Christopher Wilson <doug@somethingdoug.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.
|
||||
+234
@@ -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)
|
||||
}
|
||||
+39
@@ -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 <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (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"
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Jonathan Ong <me@jongleberry.com>
|
||||
Copyright (c) 2016 Douglas Christopher Wilson <doug@somethingdoug.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.
|
||||
+65
@@ -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"
|
||||
}
|
||||
+146
@@ -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)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "statuses",
|
||||
"description": "HTTP status utility",
|
||||
"version": "2.0.1",
|
||||
"contributors": [
|
||||
"Douglas Christopher Wilson <doug@somethingdoug.com>",
|
||||
"Jonathan Ong <me@jongleberry.com> (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"
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
| ^
|
||||
|
||||
@@ -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');
|
||||
@@ -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 |
|
||||
8 | console.log('boot', helper());
|
||||
9 | var lib = require('./lib.js');
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
10 |
|
||||
@@ -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"), {
|
||||
|
||||
Reference in New Issue
Block a user