mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat(child_process): support inline execFile callback options (#387)
* feat(child_process): lower UTF-8 execFile callback options * test(ts7): record execFile callback options fixture order
This commit is contained in:
@@ -3168,25 +3168,54 @@ export function lowerForkCall(lowerer: Lowerer, expr: ts.CallExpression, loc: Sr
|
||||
};
|
||||
}
|
||||
|
||||
/** `execFile(file[, args], callback)` — the first asynchronous callback
|
||||
/** `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,
|
||||
* stderr)`, but every declared parameter must have the Node shape. Options
|
||||
* remain fenced until their timeout/env/cwd lifecycle can share this same
|
||||
* asynchronous core without falling back to the old blocking capture. */
|
||||
* 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 {
|
||||
if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2 || expr.arguments.length > 3) {
|
||||
if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length < 2 || expr.arguments.length > 4) {
|
||||
lowerer.noLowering(
|
||||
`execFile with ${expr.arguments.length} arguments`,
|
||||
expr,
|
||||
"the supported callback forms are execFile(file, callback) and execFile(file, args, 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 argsNode = expr.arguments.length >= 3 ? expr.arguments[1] : undefined;
|
||||
const callbackNode = expr.arguments[expr.arguments.length - 1]!;
|
||||
const argv = lowerer.lowerChildArgsArg(argsNode, loc);
|
||||
if (expr.arguments.length === 4) {
|
||||
const options = expr.arguments[2]!;
|
||||
if (!ts.isObjectLiteralExpression(options)) {
|
||||
lowerer.noLowering("execFile with a non-literal options argument", options, "pass encoding and maxBuffer in an inline object literal");
|
||||
}
|
||||
const pureNumber = (node: ts.Expression): boolean =>
|
||||
ts.isNumericLiteral(node) ||
|
||||
(ts.isParenthesizedExpression(node) && pureNumber(node.expression)) ||
|
||||
(ts.isBinaryExpression(node) &&
|
||||
[ts.SyntaxKind.PlusToken, ts.SyntaxKind.MinusToken, ts.SyntaxKind.AsteriskToken, ts.SyntaxKind.SlashToken].includes(node.operatorToken.kind) &&
|
||||
pureNumber(node.left) && pureNumber(node.right));
|
||||
for (const property of options.properties) {
|
||||
const member = optionMember(property);
|
||||
if (!member) lowerer.noLowering("execFile with this options shape", property, "use plain encoding and maxBuffer properties without spreads or computed keys");
|
||||
if (member.name === "encoding") {
|
||||
if (!ts.isStringLiteral(member.value) || (member.value.text !== "utf8" && member.value.text !== "utf-8")) {
|
||||
lowerer.noLowering("execFile with a non-literal utf8 encoding", member.value, "pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)");
|
||||
}
|
||||
} else if (member.name === "maxBuffer") {
|
||||
if (!pureNumber(member.value)) {
|
||||
lowerer.noLowering("execFile 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}'`, property, "only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form");
|
||||
}
|
||||
}
|
||||
}
|
||||
const callback = lowerer.lowerExpr(callbackNode);
|
||||
if (callback.type.kind !== "func" || callback.type.rest === true || callback.type.params.length > 3) {
|
||||
lowerer.unsupported(
|
||||
|
||||
@@ -1386,8 +1386,8 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<string, Record<string, string |
|
||||
},
|
||||
child_process: {
|
||||
execFile:
|
||||
"the callback forms execFile(file, callback) and execFile(file, args, callback) lower; " +
|
||||
"options and reached no-callback calls remain fenced, while util.promisify(execFile) retains its wider options slice",
|
||||
"the callback forms execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback) lower; " +
|
||||
"maxBuffer is not enforced by the growing native capture; other options and reached no-callback calls remain fenced, while util.promisify(execFile) retains its wider options slice",
|
||||
},
|
||||
crypto: {
|
||||
...Object.fromEntries(
|
||||
|
||||
@@ -445,7 +445,7 @@
|
||||
"name": "child_process.execFile",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "the callback forms execFile(file, callback) and execFile(file, args, callback) lower; options and reached no-callback calls remain fenced, while util.promisify(execFile) retains its wider options slice"
|
||||
"note": "the callback forms execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback) lower; maxBuffer is not enforced by the growing native capture; other options and reached no-callback calls remain fenced, while util.promisify(execFile) retains its wider options slice"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.child_process.execFileSync",
|
||||
|
||||
@@ -6313,6 +6313,12 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2912-execfile-callback-options.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2912-execfile-callback-options.ts"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/2912-promisify-fs-readfile.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/2912-promisify-fs-readfile.ts"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// The inline utf8/maxBuffer callback shape preserves concurrent execution,
|
||||
// streams and error-first result on both successful and failed children.
|
||||
import { execFile } from "node:child_process";
|
||||
|
||||
const child = execFile(
|
||||
"/bin/sh",
|
||||
["-c", "printf out; printf note >&2"],
|
||||
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
|
||||
(error: Error | null, stdout, stderr) => {
|
||||
console.log("ok", error === null, JSON.stringify(stdout), JSON.stringify(stderr));
|
||||
execFile(
|
||||
"/bin/sh",
|
||||
["-c", "printf partial; printf failed >&2; exit 3"],
|
||||
{ encoding: "utf8", maxBuffer: 67108864 },
|
||||
(failed: Error | null, failedOut, failedErr) => {
|
||||
console.log("failed", failed !== null, JSON.stringify(failedOut), JSON.stringify(failedErr));
|
||||
console.log("error", failed === null ? "" : failed.message.includes("exit 3"));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
console.log("returned", child.pid !== undefined);
|
||||
console.log("scheduled");
|
||||
@@ -27,10 +27,14 @@ const c = spawn("true", [], { stdio: "ignore" });
|
||||
c.on("exit", (): number => 5);
|
||||
// Methods have no bound-value form — call on directly.
|
||||
const f = c.on;
|
||||
// The callback slice is deliberately narrower than Node's complete
|
||||
// options/optional-callback overload family.
|
||||
// The callback slice accepts inline utf8/maxBuffer options, but not all of
|
||||
// Node's options or the optional-callback overload family.
|
||||
execFile("true");
|
||||
execFile("true", [], { encoding: "utf8" }, () => {});
|
||||
execFile("true", [], { cwd: "/tmp" }, () => {});
|
||||
const bufferLimit = 1024;
|
||||
execFile("true", [], { maxBuffer: bufferLimit }, () => {});
|
||||
const getEncoding = (): "utf8" => "utf8";
|
||||
execFile("true", [], { encoding: getEncoding() }, () => {});
|
||||
// Fork targets are part of the compiled graph and therefore must resolve at
|
||||
// build time. The remaining channel is JSON-only and occupies stdio slot 3.
|
||||
fork(process.argv[1]!);
|
||||
|
||||
@@ -28,7 +28,7 @@ child-process.ts:29:7 - error SC2007: values of type '{ (event: "exit", listener
|
||||
28 | // Methods have no bound-value form — call on directly.
|
||||
29 | const f = c.on;
|
||||
| ^
|
||||
30 | // The callback slice is deliberately narrower than Node's complete
|
||||
30 | // The callback slice accepts inline utf8/maxBuffer options, but not all of
|
||||
|
||||
hint: annotate the slot with the ONE signature the program actually calls (e.g. '(x: number) => string'), or wrap the overloaded function in a single-signature arrow
|
||||
|
||||
@@ -37,49 +37,67 @@ child-process.ts:29:11 - error SC1090: child methods as values (call 'on' direct
|
||||
28 | // Methods have no bound-value form — call on directly.
|
||||
29 | const f = c.on;
|
||||
| ^~~~
|
||||
30 | // The callback slice is deliberately narrower than Node's complete
|
||||
30 | // The callback slice accepts inline utf8/maxBuffer options, but not all of
|
||||
|
||||
child-process.ts:32:1 - error SC2020: 'execFile with 1 arguments' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
31 | // options/optional-callback overload family.
|
||||
31 | // Node's options or the optional-callback overload family.
|
||||
32 | execFile("true");
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
33 | execFile("true", [], { encoding: "utf8" }, () => {});
|
||||
33 | execFile("true", [], { cwd: "/tmp" }, () => {});
|
||||
|
||||
hint: the supported callback forms are execFile(file, callback) and execFile(file, args, callback)
|
||||
hint: the supported callback forms are execFile(file, callback), execFile(file, args, callback), and execFile(file, args, { encoding: 'utf8', maxBuffer: N }, callback)
|
||||
|
||||
child-process.ts:33:1 - error SC2020: 'execFile with 4 arguments' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:33:24 - error SC2020: 'execFile option 'cwd'' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
32 | execFile("true");
|
||||
33 | execFile("true", [], { encoding: "utf8" }, () => {});
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
34 | // Fork targets are part of the compiled graph and therefore must resolve at
|
||||
33 | execFile("true", [], { cwd: "/tmp" }, () => {});
|
||||
| ^~~~~~~~~~~
|
||||
34 | const bufferLimit = 1024;
|
||||
|
||||
hint: the supported callback forms are execFile(file, callback) and execFile(file, args, callback)
|
||||
hint: only encoding: 'utf8' and a pure numeric maxBuffer are supported in the callback form
|
||||
|
||||
child-process.ts:36:6 - error SC2020: 'fork with a runtime-valued module path' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:35:35 - error SC2020: 'execFile with a non-literal maxBuffer' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
35 | // build time. The remaining channel is JSON-only and occupies stdio slot 3.
|
||||
36 | fork(process.argv[1]!);
|
||||
34 | const bufferLimit = 1024;
|
||||
35 | execFile("true", [], { maxBuffer: bufferLimit }, () => {});
|
||||
| ^~~~~~~~~~~
|
||||
36 | const getEncoding = (): "utf8" => "utf8";
|
||||
|
||||
hint: pass a numeric literal or literal arithmetic (the native capture grows without enforcing this limit)
|
||||
|
||||
child-process.ts:37:34 - error SC2020: 'execFile with a non-literal utf8 encoding' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
36 | const getEncoding = (): "utf8" => "utf8";
|
||||
37 | execFile("true", [], { encoding: getEncoding() }, () => {});
|
||||
| ^~~~~~~~~~~~~
|
||||
38 | // Fork targets are part of the compiled graph and therefore must resolve at
|
||||
|
||||
hint: pass the literal 'utf8' or 'utf-8' (other expressions might have side effects)
|
||||
|
||||
child-process.ts:40:6 - error SC2020: 'fork with a runtime-valued module path' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
39 | // build time. The remaining channel is JSON-only and occupies stdio slot 3.
|
||||
40 | fork(process.argv[1]!);
|
||||
| ^~~~~~~~~~~~~~~~
|
||||
37 | const staticWorker = new URL("./child-process.ts", import.meta.url);
|
||||
41 | const staticWorker = new URL("./child-process.ts", import.meta.url);
|
||||
|
||||
hint: resolve a relative worker with new URL("./worker.ts", import.meta.url) or fileURLToPath(...) and bind it to a const if needed
|
||||
|
||||
child-process.ts:38:41 - error SC2020: 'fork with non-JSON serialization' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:42:41 - error SC2020: 'fork with non-JSON serialization' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
37 | const staticWorker = new URL("./child-process.ts", import.meta.url);
|
||||
38 | fork(staticWorker, [], { serialization: "advanced" });
|
||||
41 | const staticWorker = new URL("./child-process.ts", import.meta.url);
|
||||
42 | fork(staticWorker, [], { serialization: "advanced" });
|
||||
| ^~~~~~~~~~
|
||||
39 | fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] });
|
||||
43 | fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] });
|
||||
|
||||
hint: the static IPC channel supports Node's default serialization or serialization: "json"
|
||||
|
||||
child-process.ts:39:64 - error SC2020: 'fork without a fourth-slot IPC channel' is part of the standard library types but has no scriptc lowering yet
|
||||
child-process.ts:43:64 - error SC2020: 'fork without a fourth-slot IPC channel' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
38 | fork(staticWorker, [], { serialization: "advanced" });
|
||||
39 | fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] });
|
||||
42 | fork(staticWorker, [], { serialization: "advanced" });
|
||||
43 | fork(staticWorker, [], { stdio: ["ignore", "ignore", "ignore", "ignore"] });
|
||||
| ^~~~~~~~
|
||||
40 | // Keep each fence on its own statement so diagnostics remain site-specific.
|
||||
44 | // Keep each fence on its own statement so diagnostics remain site-specific.
|
||||
|
||||
hint: the fourth stdio entry must be "ipc"
|
||||
Reference in New Issue
Block a user