mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
Preserve native class state and handle terminal process events (#571)
- Preserve fields, accessors, added properties, and cyclic references across untyped class boundaries. - Support platform signal names and native exception handlers that resume queued work.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
// An opaque field does not prevent reading or updating the other properties
|
||||
// of the same native instance through an untyped package boundary.
|
||||
class Base {
|
||||
cache = new Map();
|
||||
width = 40;
|
||||
child = { value: 1 };
|
||||
events = [];
|
||||
get size() {
|
||||
this.events.push("get");
|
||||
return this.width * 2;
|
||||
}
|
||||
set size(value) {
|
||||
this.events.push("set");
|
||||
this.width = value / 2;
|
||||
}
|
||||
}
|
||||
class Renderer extends Base {
|
||||
height = 24;
|
||||
get label() { return `size:${this.width}`; }
|
||||
}
|
||||
const renderer = new Renderer();
|
||||
renderer.cache.set("kept", 7);
|
||||
const view = /** @type {unknown} */ (renderer);
|
||||
console.log("dimensions", view.width, view.height, view.size, view.label);
|
||||
view.width = 60;
|
||||
view.size = 160;
|
||||
console.log("updates", renderer.width, view.width, view.size, view.label);
|
||||
console.log("state", renderer.cache.get("kept"), renderer.events.join(","));
|
||||
console.log("same", view === renderer);
|
||||
console.log("missing", view.generation === undefined);
|
||||
view.generation = 1;
|
||||
const again = /** @type {unknown} */ (renderer);
|
||||
console.log("shared", again.generation);
|
||||
again.generation = 2;
|
||||
console.log("shared update", view.generation);
|
||||
view.child.value = 9;
|
||||
console.log("child", renderer.child.value, view.child === again.child);
|
||||
for (const key of ["width", "height", "size", "label", "generation"]) console.log("computed", key, view[key]);
|
||||
for (const key of ["width", "generation"]) view[key] = 100;
|
||||
console.log("computed update", renderer.width, view.generation);
|
||||
try { view.label = "changed"; } catch (error) { console.log("readonly", error.name, error.message); }
|
||||
|
||||
class Data {
|
||||
first = 1;
|
||||
second = 2;
|
||||
}
|
||||
const data = new Data();
|
||||
const record = /** @type {unknown} */ (data);
|
||||
record.extra = 3;
|
||||
console.log("keys", Object.keys(record).join(","));
|
||||
console.log("json", JSON.stringify(record));
|
||||
console.log("descriptor", JSON.stringify(Object.getOwnPropertyDescriptor(record, "extra")));
|
||||
Object.assign(record, { first: 4, appended: 5 });
|
||||
console.log("assigned", data.first, record.extra, record.appended, JSON.stringify(record));
|
||||
@@ -0,0 +1,11 @@
|
||||
class Context {
|
||||
id = 1;
|
||||
}
|
||||
function createCycle() {
|
||||
const context = new Context();
|
||||
const view = /** @type {unknown} */ (context);
|
||||
view.self = view;
|
||||
view.nested = { owner: view };
|
||||
console.log(view.self === view, view.nested.owner === view, context.id);
|
||||
}
|
||||
for (let i = 0; i < 3; i++) createCycle();
|
||||
@@ -0,0 +1,17 @@
|
||||
class State {
|
||||
width = 1;
|
||||
get size() { console.log("get"); return this.width; }
|
||||
set size(value) { console.log("set", value); this.width = value; }
|
||||
get failed() { if (this.width > 0) throw new Error("getter"); return 0; }
|
||||
}
|
||||
const state = new State();
|
||||
const view = /** @type {unknown} */ (state);
|
||||
const key = { toString() { console.log("key"); return "size"; } };
|
||||
function value() { console.log("value"); return 2; }
|
||||
view[key] = value();
|
||||
console.log("read", view[String("size")], state.width);
|
||||
const throwingKey = { toString() { throw new Error("key conversion"); } };
|
||||
try { view[throwingKey] = value(); } catch (error) { console.log(error.message); }
|
||||
try { console.log(view.failed); } catch (error) { console.log(error.message); }
|
||||
const absent = /** @type {unknown} */ (null);
|
||||
try { absent[key] = value(); } catch (error) { console.log("absent", error.name); }
|
||||
@@ -0,0 +1,31 @@
|
||||
// Named signals use host numbers, deliver their name, and preserve callback identity.
|
||||
const signal = String("SIGWINCH");
|
||||
let deliveries = 0;
|
||||
const keepAlive = setInterval(() => {}, 1000);
|
||||
const late = () => console.log("late");
|
||||
const removed = () => console.log("removed snapshot");
|
||||
const listener = (name, number) => {
|
||||
console.log("signal", name, number > 0, ++deliveries);
|
||||
process.off(signal, removed);
|
||||
if (deliveries === 1) {
|
||||
process.on(signal, late);
|
||||
setTimeout(() => process.kill(process.pid, signal), 20);
|
||||
} else {
|
||||
process.off(signal, listener);
|
||||
process.off(signal, late);
|
||||
clearInterval(keepAlive);
|
||||
}
|
||||
};
|
||||
process.on(signal, listener);
|
||||
process.once(signal, (name) => console.log("once", name));
|
||||
process.on(signal, removed);
|
||||
process.on("SIGBREAK", late);
|
||||
process.removeListener("SIGBREAK", late);
|
||||
for (const name of ["SIGKILL", "SIGSTOP"]) {
|
||||
try {
|
||||
process.on(name, late);
|
||||
} catch (error) {
|
||||
console.log("uncatchable", error.code);
|
||||
}
|
||||
}
|
||||
setTimeout(() => process.kill(process.pid, signal), 20);
|
||||
@@ -0,0 +1,6 @@
|
||||
// @exit: 7
|
||||
process.once("uncaughtException", (error, origin) => {
|
||||
console.log("handler", error.message, origin);
|
||||
throw new Error("handler failed");
|
||||
});
|
||||
setTimeout(() => { throw new Error("timer"); }, 1);
|
||||
@@ -0,0 +1,20 @@
|
||||
let count = 0;
|
||||
const late = (error) => console.log("late", error.message);
|
||||
const removed = (error) => console.log("snapshot", error.message);
|
||||
const handler = (error, origin) => {
|
||||
console.log("handled", error.message, origin);
|
||||
process.off("uncaughtException", removed);
|
||||
if (count === 1) process.once("uncaughtException", late);
|
||||
process.nextTick(() => console.log("tick", count));
|
||||
};
|
||||
process.on("uncaughtException", handler);
|
||||
process.on("uncaughtException", removed);
|
||||
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
|
||||
const interval = setInterval(() => {
|
||||
count++;
|
||||
if (count < 3) throw new Error("interval " + count);
|
||||
clearInterval(interval);
|
||||
process.off("uncaughtException", handler);
|
||||
process.once("uncaughtException", (value, origin) => console.log("primitive", value, origin));
|
||||
setImmediate(() => { throw "immediate"; });
|
||||
}, 10);
|
||||
@@ -0,0 +1,7 @@
|
||||
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
|
||||
process.once("uncaughtException", (error, origin) => {
|
||||
console.log("handled module", error.message, origin);
|
||||
setTimeout(() => console.log("resumed"), 10);
|
||||
});
|
||||
await Promise.resolve();
|
||||
throw new Error("module");
|
||||
@@ -0,0 +1,4 @@
|
||||
// @exit: 1
|
||||
// @no-node-shims
|
||||
process.on("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
|
||||
throw new Error("unhandled");
|
||||
@@ -0,0 +1,8 @@
|
||||
process.once("uncaughtExceptionMonitor", (error, origin) => console.log("monitor", error.message, origin));
|
||||
process.once("uncaughtException", (error, origin) => {
|
||||
console.log("handled rejection", error.message, origin);
|
||||
process.once("unhandledRejection", () => { throw new Error("listener"); });
|
||||
process.once("uncaughtException", (error, origin) => console.log("handled listener", error.message, origin));
|
||||
setTimeout(() => Promise.reject(new Error("next")), 10);
|
||||
});
|
||||
Promise.reject(new Error("first"));
|
||||
@@ -0,0 +1,13 @@
|
||||
// @no-node-shims
|
||||
process.once("uncaughtExceptionMonitor", (error, origin) => {
|
||||
console.log("monitor", error.message, origin);
|
||||
});
|
||||
const removed = () => console.log("should not run");
|
||||
process.on("uncaughtException", removed);
|
||||
process.off("uncaughtException", removed);
|
||||
process.once("uncaughtException", (error, origin) => {
|
||||
console.log("handled", error instanceof Error, error.message, origin);
|
||||
process.nextTick(() => console.log("handler tick"));
|
||||
});
|
||||
setTimeout(() => console.log("survived"), 10);
|
||||
throw new Error("entry");
|
||||
@@ -143,7 +143,11 @@ function nodeOracleArgs(file: string): string[] {
|
||||
? ["--experimental-transform-types", "--disable-warning=ExperimentalWarning"]
|
||||
: [];
|
||||
const nodep = wantsNoDeprecation(file) ? ["--no-deprecation"] : [];
|
||||
return [...transform, ...nodep, "--import", comptimeShim, "--import", islandShim, nodeOracleFile(file)];
|
||||
// --import makes Node load even a CJS entry through its ESM loader,
|
||||
// changing an entry throw's uncaughtException origin to unhandledRejection.
|
||||
const shims = directiveHead(file).includes("// @no-node-shims")
|
||||
? [] : ["--import", comptimeShim, "--import", islandShim];
|
||||
return [...transform, ...nodep, ...shims, nodeOracleFile(file)];
|
||||
}
|
||||
|
||||
/** Runs a binary, tolerating an expected nonzero exit (execFile rejects on
|
||||
|
||||
@@ -231,8 +231,10 @@ async function runLinuxNode(file: string): Promise<RunResult> {
|
||||
...(directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l))
|
||||
? ["--no-deprecation"]
|
||||
: []),
|
||||
"--import", inContainer(join(repoRoot, "tests/harness/comptime-shim.mjs")),
|
||||
"--import", inContainer(join(repoRoot, "tests/harness/island-shim.mjs")),
|
||||
...(directiveHead(file).includes("// @no-node-shims") ? [] : [
|
||||
"--import", inContainer(join(repoRoot, "tests/harness/comptime-shim.mjs")),
|
||||
"--import", inContainer(join(repoRoot, "tests/harness/island-shim.mjs")),
|
||||
]),
|
||||
inContainer(nodeOracleFile(file)),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -190,7 +190,11 @@ function nodeOracleArgs(file: string): string[] {
|
||||
? ["--experimental-transform-types", "--disable-warning=ExperimentalWarning"]
|
||||
: [];
|
||||
const nodep = wantsNoDeprecation(file) ? ["--no-deprecation"] : [];
|
||||
return [...transform, ...nodep, "--import", comptimeShim, "--import", islandShim, nodeOracleFile(file)];
|
||||
// --import makes Node load even a CJS entry through its ESM loader,
|
||||
// changing an entry throw's uncaughtException origin to unhandledRejection.
|
||||
const shims = directiveHead(file).includes("// @no-node-shims")
|
||||
? [] : ["--import", comptimeShim, "--import", islandShim];
|
||||
return [...transform, ...nodep, ...shims, nodeOracleFile(file)];
|
||||
}
|
||||
|
||||
function programInputs(file: string): string[] {
|
||||
|
||||
@@ -85,6 +85,7 @@ const WINDOWS_SKIPS: Record<string, string> = {
|
||||
// stdout/stderr read "" here where Node types them null (the
|
||||
// documented spawnSync stance) — invisible on POSIX lanes where these
|
||||
// spawns succeed, exposed here where every one fails.
|
||||
"process-named-signals.cjs": "POSIX signal delivery: Windows Node cannot send SIGWINCH or register SIGSTOP",
|
||||
"1360-spawn-sync.ts": "posix-shaped: every spawn is ENOENT on Windows Node too, exposing the documented spawn-failure \"\"-vs-null stdout stance (1644 covers spawnSync here)",
|
||||
"1361-spawn-events.ts": "posix-shaped: the unlistened /bin/sh spawn failure crashes both sides, rendered differently (1646 covers spawn events here)",
|
||||
"1362-spawn-timers.ts": "posix-shaped: the unlistened /bin/sh spawn failure crashes both sides, rendered differently",
|
||||
@@ -307,8 +308,10 @@ async function runWindowsNode(file: string): Promise<RunResult> {
|
||||
const nodep = directiveHead(file).some((l) => /^\/\/ @no-deprecation\s*$/.test(l))
|
||||
? "--no-deprecation "
|
||||
: "";
|
||||
const shims = directiveHead(file).includes("// @no-node-shims")
|
||||
? "" : "--import ./comptime-shim.mjs --import ./island-shim.mjs ";
|
||||
return runOnBox(
|
||||
`node ${transform}${nodep}--import ./comptime-shim.mjs --import ./island-shim.mjs ${entry}`,
|
||||
`node ${transform}${nodep}${shims}${entry}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user