mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
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`.
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "cade1b8e6e0c5c42fcc9",
|
||||
"artifactVersion": "be8d5b094695385e34ea",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -164,9 +164,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 17,
|
||||
"partial": 18,
|
||||
"refused": 0,
|
||||
"not-implemented": 22,
|
||||
"not-implemented": 21,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -9312,9 +9312,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"tierItemCount": 3456,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1143,
|
||||
"verify-gap": 2186,
|
||||
"audit-partial": 1144,
|
||||
"verify-gap": 2185,
|
||||
"replace-refusal": 40,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -3941,12 +3941,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:child_process",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:child_process.child_process.exec",
|
||||
"tests": [
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -131,9 +131,9 @@
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 17,
|
||||
"partial": 18,
|
||||
"refused": 0,
|
||||
"not-implemented": 22,
|
||||
"not-implemented": 21,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -11663,7 +11663,10 @@
|
||||
"tests/corpus/3127-spawn-runtime-options.ts",
|
||||
"tests/corpus/3130-child-generic-streams.ts",
|
||||
"tests/corpus/3131-child-rest-listeners.js",
|
||||
"tests/corpus/3132-child-generic-stream-completion.ts"
|
||||
"tests/corpus/3132-child-generic-stream-completion.ts",
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -11732,8 +11735,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:child_process"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:child_process.child_process.exec",
|
||||
"tests": [
|
||||
"tests/corpus/5627-exec-callback.ts",
|
||||
"tests/fixtures/node-types/child-exec.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -322,6 +322,7 @@
|
||||
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
|
||||
{ "chapter": "child_process", "signatures": ["`subprocess[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.execFile(file[, args][, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/2911-execfile-callback.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.exec(command[, options][, callback])`"], "status": "partial", "evidence": ["tests/corpus/5627-exec-callback.ts", "tests/fixtures/node-types/child-exec.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["`child_process.fork(modulePath[, args][, options])`", "`subprocess.send(message[, sendHandle[, options]][, callback])`", "`subprocess.disconnect()`", "`connected` Type: {boolean} Set to `false` after `subprocess.disconnect()` is called.", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
|
||||
{ "chapter": "child_process", "signatures": ["Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/2939-child-shell-text.ts", "tests/corpus/3131-child-rest-listeners.js"] },
|
||||
{ "chapter": "child_process", "signatures": ["`stdout` Type: {stream.Readable|null|undefined}", "`stderr` Type: {stream.Readable|null|undefined}"], "status": "partial", "evidence": ["tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2939-child-shell-text.ts"] },
|
||||
|
||||
+4
-4
@@ -3033,10 +3033,10 @@ declare module "child_process" {
|
||||
args?: string[] | null,
|
||||
options?: ExecSyncOptions,
|
||||
): unknown;
|
||||
/* The async shell form — declared surface without full lowering (the
|
||||
* type-level tolerance story): harness code on paths tests don't reach
|
||||
* must typecheck; a reached call fences at its site. util.promisify
|
||||
* recognizes it statically, as it does execFile. */
|
||||
/* The async shell form. The callback forms lower like execFile's, with
|
||||
* the command line run by /bin/sh -c, and util.promisify recognizes it
|
||||
* statically, as it does execFile; reached calls without a callback
|
||||
* fence at their site. */
|
||||
export function exec(
|
||||
command: string,
|
||||
options?: ExecSyncOptions | ((error: Error | null, stdout: string, stderr: string) => void),
|
||||
|
||||
@@ -19,12 +19,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
const argv = host.emitExpr(e.args[1]!);
|
||||
const cb = host.emitExpr(callbackArg);
|
||||
host.moveTemp(cb);
|
||||
const shell = host.emitExpr(e.args[3]!);
|
||||
const adapter = host.execFileThunkFor(cbT);
|
||||
host.usesTimers = true;
|
||||
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_exec_file(ptr, ptr, ptr, ptr, i1)`);
|
||||
const out = B.tmp();
|
||||
B.line(
|
||||
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter})`,
|
||||
`${out} = call ptr @scr_exec_file(ptr ${cmd.name}, ptr ${argv.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${shell.name})`,
|
||||
);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
@@ -62,8 +62,8 @@ export function lowerBuiltinCall(
|
||||
loc: SrcLoc,
|
||||
namespaceMember?: ts.MemberName,
|
||||
): IrExpr {
|
||||
if (bi.module === "child_process" && bi.member === "execFile") {
|
||||
return lowerer.lowerExecFileCall(expr, loc);
|
||||
if (bi.module === "child_process" && (bi.member === "execFile" || bi.member === "exec")) {
|
||||
return lowerer.lowerExecFileCall(expr, loc, bi.member === "exec");
|
||||
}
|
||||
if (bi.module === "events" && bi.member === "addAbortListener") {
|
||||
return lowerAddAbortListener(lowerer, expr, loc);
|
||||
|
||||
@@ -725,35 +725,57 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
|
||||
}
|
||||
|
||||
/** `execFile(file[, args][, options], callback)` — the asynchronous callback
|
||||
* slice. The child starts immediately with stdin/stdout/stderr piped; the
|
||||
* runtime captures both outputs and invokes the error-first callback after
|
||||
* settlement. The callback may ignore a suffix of `(error, stdout,
|
||||
* slice. With `shell`, `exec(command[, options], callback)`: the command
|
||||
* line runs with /bin/sh -c, as execSync runs it, and failure messages show
|
||||
* the command line as Node's do. The child starts immediately with
|
||||
* stdin/stdout/stderr piped; the runtime captures both outputs and invokes
|
||||
* the error-first callback after settlement. The callback may ignore a suffix of `(error, stdout,
|
||||
* stderr)`, but every declared parameter must have the Node shape. Inline
|
||||
* `encoding: "utf8"` and a pure numeric `maxBuffer` are accepted; the latter
|
||||
* is not enforced by the growing native capture, as
|
||||
* with the synchronous and promisified capture forms. Other options remain
|
||||
* fenced until their lifecycle can share this asynchronous core. */
|
||||
export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
export function lowerExecFileCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
shell = false,
|
||||
): IrExpr {
|
||||
const what = shell ? "exec" : "execFile";
|
||||
if (
|
||||
expr.arguments.some(ts.isSpreadElement) ||
|
||||
expr.arguments.length < 2 ||
|
||||
expr.arguments.length > 4
|
||||
expr.arguments.length > (shell ? 3 : 4)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`execFile with ${expr.arguments.length} arguments`,
|
||||
`${what} with ${expr.arguments.length} arguments`,
|
||||
expr,
|
||||
"the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
|
||||
shell
|
||||
? "the supported callback forms are exec(command, callback) and exec(command, { encoding: 'utf8', maxBuffer: N }, callback)"
|
||||
: "the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)",
|
||||
);
|
||||
}
|
||||
const cmd = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
const argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
|
||||
const first = lowerer.lowerExprExpecting(expr.arguments[0]!, STRING);
|
||||
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
|
||||
const argv = lowerChildArgsArg(lowerer, argsNode, loc);
|
||||
if (expr.arguments.length === 4) {
|
||||
const options = expr.arguments[2]!;
|
||||
// exec: /bin/sh -c <command>, execSync's lowering (the runtime reads the
|
||||
// command line back from argv[1] for failure messages); execFile: the
|
||||
// file and its args list.
|
||||
const cmd: IrExpr = shell ? { kind: "strLit", value: "/bin/sh", type: STRING, loc } : first;
|
||||
const argv: IrExpr = shell
|
||||
? {
|
||||
kind: "arrayLit",
|
||||
elems: [{ kind: "strLit", value: "-c", type: STRING, loc }, first],
|
||||
type: arrayOf(STRING),
|
||||
loc,
|
||||
}
|
||||
: lowerChildArgsArg(lowerer, expr.arguments.length >= 3 ? expr.arguments[1] : undefined, loc);
|
||||
const optionsNode =
|
||||
expr.arguments.length === (shell ? 3 : 4) ? expr.arguments[shell ? 1 : 2] : undefined;
|
||||
if (optionsNode) {
|
||||
const options = optionsNode;
|
||||
if (!ts.isObjectLiteralExpression(options)) {
|
||||
lowerer.noLowering(
|
||||
"execFile with a non-literal options argument",
|
||||
`${what} with a non-literal options argument`,
|
||||
options,
|
||||
"pass encoding and maxBuffer in an inline object literal",
|
||||
);
|
||||
@@ -774,7 +796,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
const member = optionMember(property);
|
||||
if (!member)
|
||||
lowerer.noLowering(
|
||||
"execFile with this options shape",
|
||||
`${what} with this options shape`,
|
||||
property,
|
||||
"use plain encoding and maxBuffer properties without spreads or computed keys",
|
||||
);
|
||||
@@ -784,7 +806,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
(member.value.text !== "utf8" && member.value.text !== "utf-8")
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
"execFile with a non-literal utf8 encoding",
|
||||
`${what} with a non-literal utf8 encoding`,
|
||||
member.value,
|
||||
"pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)",
|
||||
);
|
||||
@@ -792,14 +814,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
} else if (member.name === "maxBuffer") {
|
||||
if (!pureNumber(member.value)) {
|
||||
lowerer.noLowering(
|
||||
"execFile with a non-literal maxBuffer",
|
||||
`${what} with a non-literal maxBuffer`,
|
||||
member.value,
|
||||
"pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
lowerer.noLowering(
|
||||
`execFile option '${member.name}'`,
|
||||
`${what} option '${member.name}'`,
|
||||
property,
|
||||
"only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form",
|
||||
);
|
||||
@@ -815,14 +837,14 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
"execFile callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters",
|
||||
`${what} callbacks take (error), (error, stdout), (error, stdout, stderr), or no parameters`,
|
||||
);
|
||||
}
|
||||
if (callback.type.ret.kind !== "void") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
"execFile callbacks returning a value (make the callback body a block, or return nothing)",
|
||||
`${what} callbacks returning a value (make the callback body a block, or return nothing)`,
|
||||
);
|
||||
}
|
||||
const errorParam = callback.type.params[0];
|
||||
@@ -837,7 +859,7 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`execFile callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
|
||||
`${what} callbacks whose first parameter is not 'Error | null' (got '${lowerer.fmt(errorParam)}')`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -846,9 +868,15 @@ export function lowerExecFileCall(lowerer: Lowerer, expr: ts.CallExpression, loc
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
callbackNode,
|
||||
`execFile callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
|
||||
`${what} callbacks whose ${i === 1 ? "stdout" : "stderr"} parameter is not 'string'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { kind: "libCall", fn: "cp.execFile", args: [cmd, argv, callback], type: CHILD_T, loc };
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "cp.execFile",
|
||||
args: [cmd, argv, callback, boolLit(shell, loc)],
|
||||
type: CHILD_T,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,28 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
"diff",
|
||||
]);
|
||||
|
||||
/** True for child_process.ExecException, exec's callback error. Its
|
||||
* `code` is the child's numeric exit status (null after a signal), which
|
||||
* the runtime error's string code slot does not hold, so reads, `in` tests
|
||||
* and typeof tests of `code` refuse instead of answering from the slot. */
|
||||
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
|
||||
const sym = t.getSymbol();
|
||||
return (
|
||||
sym?.name === "ExecException" &&
|
||||
lowerer.checker
|
||||
.declarationsOf(sym)
|
||||
.some((d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()))
|
||||
);
|
||||
}
|
||||
|
||||
export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never {
|
||||
return lowerer.noLowering(
|
||||
"ExecException.code",
|
||||
node,
|
||||
"spawn reports the exit status through its 'close' event",
|
||||
);
|
||||
}
|
||||
|
||||
/** `.code` and `.cause` on an error-hierarchy receiver. The latter reads
|
||||
* the hidden dyn slot installed by constructor options. The former is NodeJS.ErrnoException's
|
||||
* member (the fallback declares the same shape): the runtime Error's
|
||||
@@ -122,6 +144,9 @@ export function lowerErrorCodeProperty(
|
||||
!(recvT.className === "%Error" && lowerer.typeOf(expr.expression).isIntersectionType())
|
||||
)
|
||||
return null;
|
||||
if (expr.name.text === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.expression))) {
|
||||
refuseExecExceptionCode(lowerer, expr);
|
||||
}
|
||||
const rawReceiver = lowerer.lowerExpr(expr.expression);
|
||||
const receiver =
|
||||
rawReceiver.type.kind === "dyn"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -124,6 +124,15 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
|
||||
continue;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
|
||||
// A check that only reads assigned fields and throws or does nothing
|
||||
// with the receiver (`if (this.size < 0) throw new RangeError()`)
|
||||
// does not expose the instance; assignments in its branches are not
|
||||
// definite and still count as exposures.
|
||||
if (
|
||||
(ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
|
||||
!exposes(stmt, depth)
|
||||
)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -143,7 +143,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
|
||||
@@ -468,6 +468,11 @@ export interface ClassInfo {
|
||||
* unassigned read throws the catchable TypeError instead of yielding
|
||||
* an undefined the declared type cannot hold (SEMANTICS.md). */
|
||||
deferredInitFields?: Set<string>;
|
||||
/** Fields (inherited included) that code construction exposes the
|
||||
* instance to can read before their assignment: the slot is the
|
||||
* undefined-armed union, allocation writes undefined, and reads yield the
|
||||
* union value, so an early read sees undefined as in Node. */
|
||||
earlyReadFields?: Set<string>;
|
||||
/** True when construction may hand the instance to other code (a method
|
||||
* call, an argument, a callback, or a base constructor that does) — a
|
||||
* subclass's own fields can then be read before their assignment. */
|
||||
@@ -3473,55 +3478,54 @@ export function collectClassShapeInner(
|
||||
const derived =
|
||||
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
|
||||
false;
|
||||
// Generic instances and mixins share layouts across declarations, so
|
||||
// they only report whether construction exposes the instance.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } =
|
||||
familyMode || mixin
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
)
|
||||
// A generic family is not emitted; its instances and each mixin
|
||||
// application analyze their own construction with their actual base.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } = familyMode
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
)
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
)
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
// The deferred definite-assignment check: a field on the unguarded
|
||||
// list passes only with an unconditional `this.x = ...` at the
|
||||
// constructor's TOP LEVEL — the same standard the JS-class path
|
||||
@@ -3577,18 +3581,30 @@ export function collectClassShapeInner(
|
||||
}
|
||||
}
|
||||
|
||||
if (escape.assigned && !inst) {
|
||||
// Fields assigned after construction can expose the instance (a method
|
||||
// call, a base constructor that calls an override) can be read while
|
||||
// Node still holds undefined. Their slots admit undefined, allocation
|
||||
// writes it, and reads yield it, as in Node.
|
||||
const earlyReadFields = new Set<string>(base?.earlyReadFields ?? []);
|
||||
if (escape.assigned && escape.escapes) {
|
||||
for (const f of fieldOrder) {
|
||||
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
|
||||
if (paramProps.some((pp) => pp.name === f.name)) continue;
|
||||
const declared = fields.get(f.name);
|
||||
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
|
||||
if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
|
||||
// A declared `T | undefined` field already starts undefined.
|
||||
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
|
||||
continue;
|
||||
const armed = lowerer.withUndefinedArm(declared);
|
||||
if (armed === null || armed.kind !== "union") continue;
|
||||
const armed = lowerer.runtimeOptionalType(declared);
|
||||
if (armed.kind !== "union") continue;
|
||||
fields.set(f.name, armed);
|
||||
f.type = armed;
|
||||
deferredInitFields.add(f.name);
|
||||
// Primitive and union slots yield the undefined value itself.
|
||||
// Reference slots keep the deferred-init reads: consumers that
|
||||
// observe undefined (printing, equality, typeof, nullish defaults)
|
||||
// see it, and member access on it throws a TypeError, as in Node.
|
||||
if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
|
||||
earlyReadFields.add(f.name);
|
||||
else deferredInitFields.add(f.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3693,6 +3709,43 @@ export function collectClassShapeInner(
|
||||
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
|
||||
)
|
||||
: null;
|
||||
// Constructor statements that run after the instance may have been
|
||||
// exposed (a method call, `this` passed along, a base constructor
|
||||
// that exposes it): a field they create can be read before its
|
||||
// assignment, while Node holds undefined.
|
||||
const exposedBefore = new Set<ts.Statement>();
|
||||
{
|
||||
let exposed = false;
|
||||
for (const stmt of ctor?.body?.statements ?? []) {
|
||||
const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
|
||||
if (
|
||||
expression &&
|
||||
ts.isCallExpression(expression) &&
|
||||
expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
) {
|
||||
if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
|
||||
exposed = true;
|
||||
continue;
|
||||
}
|
||||
const assigned =
|
||||
expression &&
|
||||
ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(expression.left) &&
|
||||
expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
? expression
|
||||
: null;
|
||||
if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
|
||||
const plainAssignment =
|
||||
assigned !== null &&
|
||||
ts.isPropertyAccessExpression(assigned.left) &&
|
||||
!mentionsReceiver(assigned.right) &&
|
||||
!hasAccessor(assigned.left.name.text) &&
|
||||
!methods.has(assigned.left.name.text) &&
|
||||
!lowerer.findMethodOn(base, assigned.left.name.text);
|
||||
if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
|
||||
}
|
||||
}
|
||||
if (ctor?.body) {
|
||||
for (const stmt of ctor.body.statements) {
|
||||
const lhs =
|
||||
@@ -3763,8 +3816,15 @@ export function collectClassShapeInner(
|
||||
type = inheritedJsFieldType(lowerer, decl, name, type);
|
||||
// super() can invoke a subclass setter or method before its
|
||||
// constructor assignments run. These slots start undefined.
|
||||
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
|
||||
type = DYN;
|
||||
const sealed = sealedPrefix?.created.has(name) === true;
|
||||
if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
|
||||
if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
|
||||
const armed = lowerer.runtimeOptionalType(type);
|
||||
if (armed.kind === "union") {
|
||||
type = armed;
|
||||
earlyReadFields.add(name);
|
||||
}
|
||||
}
|
||||
fields.set(name, type);
|
||||
fieldOrder.push({ name, type, initializer: undefined });
|
||||
continue;
|
||||
@@ -4025,6 +4085,7 @@ export function collectClassShapeInner(
|
||||
? { classDecorators: { nodes: classDecoratorNodes } }
|
||||
: {}),
|
||||
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
|
||||
...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
|
||||
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
||||
};
|
||||
// GENERIC members get their declaring-class backlink now that the
|
||||
@@ -6199,6 +6260,33 @@ function overrideReturnAbi(
|
||||
return declared;
|
||||
}
|
||||
|
||||
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
|
||||
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"f64",
|
||||
"bool",
|
||||
"string",
|
||||
"bigint",
|
||||
]);
|
||||
|
||||
/** Field types whose slot can admit undefined for reads before the field's
|
||||
* assignment. Date values are scalars, so their unassigned slot is memory
|
||||
* safe; checked-dynamic slots already hold undefined. */
|
||||
function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (isUnitType(type)) return false;
|
||||
switch (type.kind) {
|
||||
case "dyn":
|
||||
case "void":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "caught":
|
||||
return false;
|
||||
case "object":
|
||||
return lowerer.classes.get(type.className)?.def.runtime !== true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** The nearest declaration of `name` at or above `info` — the method a
|
||||
* receiver of that static class runs when nothing below overrides it. */
|
||||
export function findMethodOn(
|
||||
@@ -7628,7 +7716,11 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
|
||||
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
|
||||
className: info.def.name,
|
||||
field: pp.name,
|
||||
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
// An early-readable field's slot admits undefined (earlyReadFields).
|
||||
value: lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
info.fields.get(pp.name) ?? local!.type,
|
||||
),
|
||||
loc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { lowerReadlineProperty } from "./builtins/readline.js";
|
||||
import { lowerBlobInstanceof } from "./builtins/blob.js";
|
||||
import { lowerKeyObjectInstanceof } from "./builtins/key-object.js";
|
||||
import { lowerNetAddressInstanceof, lowerNetAddressStaticRead } from "./builtins/net-address.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "./builtins/errors.js";
|
||||
import { STATS_TYPE_TESTS } from "./builtins/filesystem.js";
|
||||
import { threadsImportOf } from "../threads-import.js";
|
||||
import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js";
|
||||
@@ -3133,6 +3134,51 @@ export function lowerTernary(
|
||||
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
|
||||
* unrepresentable without a re-tag), or to nothing (`never` in an
|
||||
* exhaustive default) leaves the expression union-typed. */
|
||||
/** Uses that read an early-read field's value as Node does when it is
|
||||
* undefined: printing, templates, operators, tests, member access, calls of
|
||||
* program functions (whose parameters the optional-value analysis widens),
|
||||
* and stores into bindings, fields and returns. Other uses, such as library
|
||||
* arguments, extract the declared type with a check. */
|
||||
function observesEarlyRead(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (
|
||||
ts.isTemplateSpan(parent) ||
|
||||
ts.isBinaryExpression(parent) ||
|
||||
ts.isPrefixUnaryExpression(parent) ||
|
||||
ts.isPostfixUnaryExpression(parent) ||
|
||||
ts.isTypeOfExpression(parent) ||
|
||||
ts.isConditionalExpression(parent) ||
|
||||
ts.isIfStatement(parent) ||
|
||||
ts.isWhileStatement(parent) ||
|
||||
ts.isReturnStatement(parent) ||
|
||||
ts.isVariableDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
(ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
|
||||
(ts.isElementAccessExpression(parent) && parent.expression === child)
|
||||
)
|
||||
return true;
|
||||
if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
|
||||
const callee = parent.expression;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "console"
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
!ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
|
||||
if (
|
||||
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
|
||||
@@ -3343,6 +3389,25 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
if (expr.type.kind !== "union") return expr;
|
||||
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
|
||||
return expr;
|
||||
// A field read before its assignment holds undefined although its
|
||||
// declared type omits it; the declared type is no narrowing proof. A flow
|
||||
// narrowing past the declared type (`if (this.x !== null)`) extracts the
|
||||
// narrowed arm with a check, since the slot may still hold undefined.
|
||||
if (
|
||||
expr.kind === "fieldGet" &&
|
||||
lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
|
||||
) {
|
||||
const symbol = ts.isPropertyAccessExpression(node)
|
||||
? lowerer.checker.getSymbolAtLocation(node.name)
|
||||
: undefined;
|
||||
const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
|
||||
const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowedArm === null || narrowedArm.kind === "union") return expr;
|
||||
if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
|
||||
return expr;
|
||||
const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
|
||||
}
|
||||
|
||||
// Represented fields and index values can include undefined even when
|
||||
// checker inference or lib.d.ts declares strings (RegExp captures).
|
||||
@@ -11326,6 +11391,7 @@ function lowerErrorCodeTypeofTest(
|
||||
recv = recv.expression;
|
||||
const errT = errorRootedObjectOf(lowerer, lowerer.typeOf(recv));
|
||||
if (!errT) continue;
|
||||
if (isExecExceptionType(lowerer, lowerer.typeOf(recv))) refuseExecExceptionCode(lowerer, prop);
|
||||
const caught = lowerer.caughtLocalOf(recv);
|
||||
let receiver: IrExpr;
|
||||
if (caught) {
|
||||
@@ -12928,6 +12994,9 @@ function lowerInExpression(lowerer: Lowerer, expr: ts.BinaryExpression, loc: Src
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (key === "code" && isExecExceptionType(lowerer, lowerer.typeOf(expr.right))) {
|
||||
refuseExecExceptionCode(lowerer, expr);
|
||||
}
|
||||
let recv = lowerer.lowerExpr(expr.right);
|
||||
const classInfo =
|
||||
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
|
||||
@@ -15103,6 +15172,20 @@ export function fieldGetExpr(
|
||||
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
|
||||
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
|
||||
}
|
||||
// An early-read field (ClassInfo.earlyReadFields) yields its undefined
|
||||
// where the use reads it as Node does; elsewhere the declared type is
|
||||
// extracted with a check.
|
||||
if (
|
||||
target.fieldType.kind === "union" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
|
||||
!observesEarlyRead(blame)
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
|
||||
if (declared && declared.kind !== "union") {
|
||||
const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
|
||||
if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
return {
|
||||
@@ -15643,6 +15726,22 @@ function lowerFieldCompoundValue(
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = lowerer.ensureString(rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (
|
||||
op === "+" &&
|
||||
read.type.kind === "union" &&
|
||||
lowerer.stripUndefinedArm(read.type).kind === "string" &&
|
||||
target.container === "class" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
|
||||
) {
|
||||
// A string field read before its assignment holds undefined, which
|
||||
// concatenates as "undefined".
|
||||
value = {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(read, access),
|
||||
right: lowerer.ensureString(rhs, rhsNode ?? access),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
} else if (bigintValue) {
|
||||
|
||||
@@ -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")
|
||||
@@ -3933,6 +3948,18 @@ export class Lowerer {
|
||||
if (!s) return null;
|
||||
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
|
||||
};
|
||||
// Fields that can be read before their assignment hold undefined then;
|
||||
// their reads are runtime-optional values like missing array reads.
|
||||
for (const info of this.classes.values()) {
|
||||
if (!info.earlyReadFields || !info.decl?.name) continue;
|
||||
const classSymbol = symbolOf(info.decl.name);
|
||||
if (!classSymbol) continue;
|
||||
const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol);
|
||||
for (const name of info.earlyReadFields) {
|
||||
const property = this.checker.getPropertyOfType(instance, name);
|
||||
if (property) optionalSymbols.add(property);
|
||||
}
|
||||
}
|
||||
const signatureOf = (node: ts.Identifier): FnSig | null => {
|
||||
const symbol = symbolOf(node);
|
||||
if (!symbol) return null;
|
||||
@@ -6328,6 +6355,7 @@ export class Lowerer {
|
||||
// slots), uniformly across params/locals/globals/fields/body types so
|
||||
// every producer and consumer agrees. Programs with no unregistered
|
||||
// reference are untouched — byte-stability holds.
|
||||
checkRequireBindingAccesses(this, functions);
|
||||
const module: IrModule | null =
|
||||
this.diags.length > 0
|
||||
? null
|
||||
@@ -11759,8 +11787,8 @@ export class Lowerer {
|
||||
return lowerSpawnCall(this, expr, loc);
|
||||
}
|
||||
|
||||
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
return lowerExecFileCall(this, expr, loc);
|
||||
lowerExecFileCall(expr: ts.CallExpression, loc: SrcLoc, shell = false): IrExpr {
|
||||
return lowerExecFileCall(this, expr, loc, shell);
|
||||
}
|
||||
|
||||
lowerExecSyncCall(expr: ts.CallExpression, shell: boolean, loc: SrcLoc): IrExpr {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1386,6 +1386,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
// child_process.ExecException, exec's callback error, is an interface
|
||||
// extending Error with optional metadata members; the value is one
|
||||
// ordinary Error as well.
|
||||
const execException = type.getSymbol();
|
||||
if (
|
||||
execException?.name === "ExecException" &&
|
||||
checker
|
||||
.declarationsOf(execException)
|
||||
.some(
|
||||
(decl) =>
|
||||
ts.isInterfaceDeclaration(decl) &&
|
||||
ctx.isStdlibFile(decl.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(decl, "child_process"),
|
||||
)
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
|
||||
@@ -2343,8 +2343,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** child_process.execFile's callback slice: starts an all-piped child,
|
||||
* captures stdout/stderr, and moves the error-first callback into the
|
||||
* child registry. The callback shape is program-dependent and checked
|
||||
* by the validator/backends. */
|
||||
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null], result: CHILD_T },
|
||||
* by the validator/backends. The last argument is exec's shell flag:
|
||||
* the argv is ["-c", command] and failure messages show the command. */
|
||||
"cp.execFile": { argTypes: [STRING, arrayOf(STRING), null, BOOL], result: CHILD_T },
|
||||
// spawn's options form: per-slot stdio modes (0 ignore / 1 inherit /
|
||||
// 2 fd) with the out/err fds for mode 2, detached, env replacement
|
||||
// pairs, cwd ("" = inherit).
|
||||
|
||||
@@ -951,6 +951,7 @@ test("library callbacks retain child, specialized, and generic result diagnostic
|
||||
{ kind: "strLit", value: "tool", type: STRING, loc },
|
||||
{ kind: "arrayLit", elems: [], type: arrayOf(STRING), loc },
|
||||
{ kind: "boolLit", value: true, type: F64, loc },
|
||||
{ kind: "boolLit", value: false, type: BOOL, loc },
|
||||
],
|
||||
};
|
||||
expect(validateModule(expressionModule(expr, [])).map((error) => error.message)).toEqual([
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import type { IrModule } from "../src/ir/ir.js";
|
||||
|
||||
async function lower(source: string, file = "main.ts"): Promise<IrModule> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-early-field-reads-"));
|
||||
try {
|
||||
const entry = join(dir, file),
|
||||
output = join(dir, "main.ir.json");
|
||||
await writeFile(entry, source);
|
||||
const result = await compile(entry, { outDir: dir, outPath: output, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return module;
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/** Field name → IR kind for a class. */
|
||||
function fields(module: IrModule, name: string): string {
|
||||
const cls = module.classes.find((candidate) => candidate.name === name);
|
||||
expect(cls, name).toBeDefined();
|
||||
return cls!.fields
|
||||
.filter((field) => !field.name.startsWith("%"))
|
||||
.map((field) => `${field.name}:${field.type.kind}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
test("fields assigned after construction can call an override admit undefined", async () => {
|
||||
const module = await lower(`
|
||||
abstract class Base {
|
||||
constructor() {
|
||||
this.describe();
|
||||
}
|
||||
abstract describe(): void;
|
||||
}
|
||||
class Panel extends Base {
|
||||
label: string;
|
||||
width: number;
|
||||
count = 0;
|
||||
constructor() {
|
||||
super();
|
||||
this.label = "panel";
|
||||
this.width = 2;
|
||||
}
|
||||
describe(): void {
|
||||
console.log(this.label, this.width, this.count);
|
||||
}
|
||||
}
|
||||
class Plain {
|
||||
label: string;
|
||||
width: number;
|
||||
constructor() {
|
||||
this.label = "plain";
|
||||
this.width = 1;
|
||||
this.describe();
|
||||
}
|
||||
describe(): void {
|
||||
console.log(this.label, this.width);
|
||||
}
|
||||
}
|
||||
new Panel();
|
||||
new Plain();
|
||||
`);
|
||||
expect(fields(module, "Panel")).toBe("label:union width:union count:union");
|
||||
expect(fields(module, "Plain")).toBe("label:string width:f64");
|
||||
});
|
||||
|
||||
test("JavaScript fields created after the constructor calls a method admit undefined", async () => {
|
||||
const module = await lower(
|
||||
`
|
||||
class Widget {
|
||||
constructor() {
|
||||
this.ready = true;
|
||||
this.render();
|
||||
this.title = "widget";
|
||||
}
|
||||
render() {
|
||||
console.log(this.ready, this.title);
|
||||
}
|
||||
}
|
||||
new Widget();
|
||||
`,
|
||||
"main.js",
|
||||
);
|
||||
expect(fields(module, "Widget")).toBe("ready:bool title:union");
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5627-exec-callback.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5943-fields-read-before-assignment.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5944-js-fields-read-before-assignment.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+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"
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-exec-code-fences.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/child-exec.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -2266,14 +2266,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn) {
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
@@ -4911,14 +4911,14 @@ static void scr_exec_file_fire(ScrChild *c) {
|
||||
}
|
||||
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb,
|
||||
ScrExecFileFn fn) {
|
||||
ScrExecFileFn fn, bool shell) {
|
||||
ScrStr *empty = scr_str_new("", 0);
|
||||
ScrChild *c = scr_spawn_opts(cmd, args, 3, 3, 3, 0, 0, false, false, false,
|
||||
NULL, empty);
|
||||
scr_str_release(empty);
|
||||
c->exec_cb = cb;
|
||||
c->exec_fn = fn;
|
||||
char *display = scr_exec_display(cmd, args, false);
|
||||
char *display = scr_exec_display(cmd, args, shell);
|
||||
c->exec_display = scr_str_new(display, strlen(display));
|
||||
free(display);
|
||||
scr_child_stream_capture_begin(c->out_stream);
|
||||
|
||||
@@ -155,9 +155,11 @@ ScrChild *scr_spawn_dynamic(ScrStr *cmd, ScrArr *args, const ScrDyn *options);
|
||||
void scr_child_on_spawn(ScrChild *child, ScrClosure *cb); /* moves zero-argument callback */
|
||||
/* The callback slice: file + args, default options. The returned child has
|
||||
* all three stdio slots piped; stdout/stderr are captured internally and
|
||||
* the callback moves into the child registry until settlement. */
|
||||
* the callback moves into the child registry until settlement. With shell
|
||||
* (exec), args is ["-c", command] for /bin/sh and failure messages show
|
||||
* the command, as for scr_exec_sync. */
|
||||
ScrChild *scr_exec_file(ScrStr *cmd, ScrArr *args, ScrClosure *cb /*moves*/,
|
||||
ScrExecFileFn fn);
|
||||
ScrExecFileFn fn, bool shell);
|
||||
ScrChild *scr_child_retain(ScrChild *c);
|
||||
void scr_child_release(ScrChild *c);
|
||||
void *scr_child_retain_v(void *p);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// child_process.exec's callback forms: the command line runs with
|
||||
// /bin/sh -c, the call returns a live ChildProcess immediately, utf8
|
||||
// stdout/stderr are captured, and the error-first callback fires after the
|
||||
// current turn. A non-zero exit (a missing command is the shell's 127)
|
||||
// reports "Command failed: <command>\n<stderr>".
|
||||
import { exec } from "node:child_process";
|
||||
import * as childProcess from "node:child_process";
|
||||
|
||||
const child = exec("printf out; printf err >&2", (error, stdout, stderr) => {
|
||||
console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr));
|
||||
exec("printf 'a\\nb\\nc\\n' | wc -l", (pipeError, lines) => {
|
||||
console.log("pipe", pipeError === null, lines.trim());
|
||||
exec(`printf '%s|' "a b" 'c d' e`, { encoding: "utf8", maxBuffer: 1024 * 1024 }, (_, quoted) => {
|
||||
console.log("quoting", quoted);
|
||||
exec("printf partial; printf failed >&2; exit 3", (failed, failedOut, failedErr) => {
|
||||
console.log("fail", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr));
|
||||
console.log("fail-message", JSON.stringify(failed === null ? "" : failed.message));
|
||||
exec("exit 1", (silent) => {
|
||||
console.log("silent", JSON.stringify(silent === null ? "" : silent.message));
|
||||
exec("scriptc-definitely-not-a-command", (missing, missingOut) => {
|
||||
const message = missing === null ? "" : missing.message;
|
||||
console.log("missing", missing !== null, JSON.stringify(missingOut));
|
||||
console.log("missing-message", message.split("\n")[0]);
|
||||
exec("true", () => {
|
||||
console.log("zero-parameter callback");
|
||||
childProcess.exec("printf namespace", (_, out) => console.log(out));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log("returned", child.pid !== undefined, child.stdout !== null, child.stderr !== null);
|
||||
console.log("scheduled");
|
||||
@@ -0,0 +1,181 @@
|
||||
// @transform-types
|
||||
// Fields that a base constructor (or the constructor itself) can read
|
||||
// through an override or a method call before they are assigned hold
|
||||
// undefined then, as in Node, for every field kind. After construction
|
||||
// the fields read and update normally.
|
||||
class Point {
|
||||
x: number;
|
||||
constructor(x: number) {
|
||||
this.x = x;
|
||||
}
|
||||
}
|
||||
function show(label: string, value: number): string {
|
||||
return `${label}=${value}`;
|
||||
}
|
||||
abstract class Widget {
|
||||
log: string[] = [];
|
||||
constructor() {
|
||||
this.describe("base");
|
||||
}
|
||||
abstract describe(when: string): void;
|
||||
}
|
||||
class Panel extends Widget {
|
||||
label: string;
|
||||
width: number;
|
||||
visible: boolean;
|
||||
origin: Point;
|
||||
items: number[];
|
||||
options: { depth: number };
|
||||
key: string | number;
|
||||
index: Map<string, number>;
|
||||
tags: Set<string>;
|
||||
count = 5;
|
||||
constructor(label: string, readonly id: string) {
|
||||
super();
|
||||
this.label = label;
|
||||
this.width = 10;
|
||||
this.visible = true;
|
||||
this.origin = new Point(1);
|
||||
this.items = [1, 2];
|
||||
this.options = { depth: 2 };
|
||||
this.key = 7;
|
||||
this.index = new Map([["a", 1]]);
|
||||
this.tags = new Set(["t"]);
|
||||
this.describe("ctor");
|
||||
}
|
||||
describe(when: string): void {
|
||||
console.log(when, this.label, this.width, this.visible, this.origin, this.items, this.options, this.key, this.count, this.id);
|
||||
console.log(when, `${this.label}:${this.width}`, this.width + 1, this.count * 2, this.label === undefined, typeof this.items, this.origin?.x, this.options?.depth ?? -1, this.index?.size, this.tags?.has("t"));
|
||||
console.log(when, show(this.label, this.width), String(this.key), [this.label].length);
|
||||
try {
|
||||
console.log(this.label.length, this.items.length);
|
||||
} catch (error) {
|
||||
console.log(when, (error as Error).message);
|
||||
}
|
||||
}
|
||||
grow(): number {
|
||||
this.count++;
|
||||
this.width += 5;
|
||||
this.label += "!";
|
||||
this.items.push(this.width);
|
||||
return this.count + this.width + this.items.length;
|
||||
}
|
||||
}
|
||||
const panel = new Panel("main", "p1");
|
||||
console.log(panel.grow(), panel.label, panel.items, panel.count, panel.width);
|
||||
|
||||
class Plain {
|
||||
total: number;
|
||||
name: string;
|
||||
constructor() {
|
||||
this.total = 1;
|
||||
this.name = "plain";
|
||||
this.bump();
|
||||
}
|
||||
bump(): void {
|
||||
this.total++;
|
||||
}
|
||||
}
|
||||
const plain = new Plain();
|
||||
console.log(plain.total, plain.name);
|
||||
|
||||
class Toucher {
|
||||
constructor() {
|
||||
this.touch();
|
||||
}
|
||||
touch(): void {}
|
||||
}
|
||||
class Counter extends Toucher {
|
||||
count = 0;
|
||||
label: string;
|
||||
items: string[];
|
||||
constructor() {
|
||||
super();
|
||||
this.label = "c";
|
||||
this.items = [];
|
||||
}
|
||||
override touch(): void {
|
||||
this.count++;
|
||||
this.label += "x";
|
||||
console.log(this.count, this.label, Number.isNaN(this.count));
|
||||
try {
|
||||
this.items.push("a");
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
const c = new Counter();
|
||||
c.touch();
|
||||
console.log(c.count, c.label, c.items);
|
||||
|
||||
class Shower {
|
||||
constructor() {
|
||||
this.show();
|
||||
}
|
||||
show(): void {}
|
||||
}
|
||||
class Box<T> extends Shower {
|
||||
label: string;
|
||||
value: T;
|
||||
constructor(value: T) {
|
||||
super();
|
||||
this.label = "box";
|
||||
this.value = value;
|
||||
}
|
||||
override show(): void {
|
||||
console.log("show", this.label, this.value);
|
||||
}
|
||||
}
|
||||
new Box<number>(3).show();
|
||||
new Box<string>("s").show();
|
||||
|
||||
type Ctor<T = {}> = new (...args: any[]) => T;
|
||||
class Viewer {
|
||||
constructor() {
|
||||
this.show();
|
||||
}
|
||||
show(): void {}
|
||||
}
|
||||
function Labeled<B extends Ctor<Viewer>>(Parent: B) {
|
||||
return class extends Parent {
|
||||
label = "mixed";
|
||||
override show(): void {
|
||||
console.log("show", this.label);
|
||||
}
|
||||
};
|
||||
}
|
||||
const L = Labeled(Viewer);
|
||||
new L().show();
|
||||
|
||||
interface Crash { loc: number; message: string }
|
||||
class Setupper {
|
||||
constructor() {
|
||||
this.setup();
|
||||
}
|
||||
setup(): void {}
|
||||
}
|
||||
class Host extends Setupper {
|
||||
crash: Crash | null;
|
||||
name: string | null;
|
||||
constructor(c: Crash | null) {
|
||||
super();
|
||||
this.crash = c;
|
||||
this.name = "host";
|
||||
}
|
||||
override setup(): void {
|
||||
console.log("setup", this.crash, this.name);
|
||||
if (this.crash !== null) {
|
||||
try {
|
||||
console.log(this.crash.loc);
|
||||
} catch (error) {
|
||||
console.log(error instanceof TypeError);
|
||||
}
|
||||
}
|
||||
}
|
||||
report(): string {
|
||||
if (this.crash) return `${this.crash.message} at ${this.crash.loc}`;
|
||||
return this.name === null ? "none" : this.name.toUpperCase();
|
||||
}
|
||||
}
|
||||
console.log(new Host({ loc: 3, message: "boom" }).report(), new Host(null).report());
|
||||
@@ -0,0 +1,28 @@
|
||||
// JavaScript class fields that the constructor creates after it calls a
|
||||
// method, or after a base constructor that does, hold undefined until
|
||||
// they are assigned, as in Node.
|
||||
class Widget {
|
||||
constructor(name) {
|
||||
this.render();
|
||||
this.name = name;
|
||||
this.size = 3;
|
||||
this.title = "widget";
|
||||
this.parts = ["a"];
|
||||
this.meta = { id: 1 };
|
||||
}
|
||||
render() {
|
||||
console.log("render", this.name, this.title, this.size, this.parts, this.meta, this.size + 1, `${this.name}!`);
|
||||
}
|
||||
}
|
||||
class Big extends Widget {
|
||||
constructor() {
|
||||
super("big");
|
||||
this.extra = 2;
|
||||
}
|
||||
render() {
|
||||
super.render();
|
||||
console.log("big", this.extra);
|
||||
}
|
||||
}
|
||||
new Widget("w").render();
|
||||
new Big().render();
|
||||
@@ -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,8 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
exec("exit 3", (error) => {
|
||||
if (error === null) return;
|
||||
console.log(error.code);
|
||||
console.log("code" in error);
|
||||
console.log(typeof error.code === "number");
|
||||
});
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
exec("printf typed-exec; printf err >&2", (error, stdout, stderr) => {
|
||||
if (error !== null) throw error;
|
||||
console.log(stdout, stderr);
|
||||
exec("exit 2", { encoding: "utf8" }, (failed) => {
|
||||
console.log(JSON.stringify(failed === null ? "" : failed.message));
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user