@genroc/eval-node 0.0.0-edge.ea911c5 → 0.0.0-edge.eaf0756c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +114 -289
- package/dist/eval.js +27 -15
- package/dist/import.js +119 -119
- package/dist/realm.js +12 -17
- package/dist/worker.js +80 -75
- package/eval.ts +30 -21
- package/import.ts +175 -148
- package/package.json +1 -1
- package/realm.ts +12 -17
- package/worker.ts +88 -79
package/dist/import.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The
|
|
3
|
-
// with
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// code; "build" typechecks and bundles. A separate types hook would mean a second `tsc`
|
|
8
|
-
// over the same project.
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
// The typed-phase resolver: manifest on stdin, `{"values": [...]}` on stdout, diagnostic on stderr
|
|
3
|
+
// with a non-zero exit (specs/source-resolution.md). Two modes share one `tsc` pass: "generate"
|
|
4
|
+
// writes declarations only, "resolve" typechecks and bundles.
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { access, writeFile } from "node:fs/promises";
|
|
11
7
|
import { builtinModules } from "node:module";
|
|
12
|
-
import { dirname, join,
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
9
|
import commonjs from "@rollup/plugin-commonjs";
|
|
15
10
|
import json from "@rollup/plugin-json";
|
|
16
11
|
import { nodeResolve } from "@rollup/plugin-node-resolve";
|
|
@@ -29,11 +24,6 @@ async function exists(path) {
|
|
|
29
24
|
return false;
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
|
-
/** Creates the parent directory, which `.genroc-cache/` relies on: nothing else makes it. */
|
|
33
|
-
async function write(path, content) {
|
|
34
|
-
await mkdir(dirname(path), { recursive: true });
|
|
35
|
-
await writeFile(path, content);
|
|
36
|
-
}
|
|
37
27
|
// ── JSON Schema → TypeScript ───────────────────────────────────────────────────
|
|
38
28
|
/** Only genroc's keyword set is handled; anything else its strict decoder would have
|
|
39
29
|
* refused before this ran (internal/schema, allowedKeywords). */
|
|
@@ -89,15 +79,17 @@ function objectType(s, used) {
|
|
|
89
79
|
const doc = typeof sub.description === "string"
|
|
90
80
|
? ` /** ${sub.description} */\n`
|
|
91
81
|
: "";
|
|
92
|
-
lines.push(`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${tsType(sub, used)};`);
|
|
82
|
+
lines.push(`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${nest(tsType(sub, used))};`);
|
|
93
83
|
}
|
|
94
84
|
if (s.additionalProperties && typeof s.additionalProperties === "object") {
|
|
95
|
-
lines.push(` [key: string]: ${tsType(s.additionalProperties, used)};`);
|
|
85
|
+
lines.push(` [key: string]: ${nest(tsType(s.additionalProperties, used))};`);
|
|
96
86
|
}
|
|
97
87
|
if (lines.length === 0)
|
|
98
88
|
return "Record<string, unknown>";
|
|
99
89
|
return `{\n${lines.join("\n")}\n}`;
|
|
100
90
|
}
|
|
91
|
+
/** Every line but the first, which is already placed by the property that opens it. */
|
|
92
|
+
const nest = (t) => t.replace(/\n/g, "\n ");
|
|
101
93
|
function union(parts) {
|
|
102
94
|
const seen = [...new Set(parts)];
|
|
103
95
|
return seen.length === 0 ? "unknown" : seen.join(" | ");
|
|
@@ -105,28 +97,14 @@ function union(parts) {
|
|
|
105
97
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
106
98
|
const propKey = (k) => (IDENT.test(k) ? k : JSON.stringify(k));
|
|
107
99
|
const identifier = (n) => IDENT.test(n) ? n : `Def_${n.replace(/[^A-Za-z0-9_$]/g, "_")}`;
|
|
108
|
-
function deref(s, defs) {
|
|
109
|
-
let cur = s;
|
|
110
|
-
for (let i = 0; cur && typeof cur.$ref === "string" && i < 16; i++) {
|
|
111
|
-
cur = defs[cur.$ref.replace(/^#\/\$defs\//, "")];
|
|
112
|
-
}
|
|
113
|
-
return cur;
|
|
114
|
-
}
|
|
115
|
-
/** The manifest's `input` is the type of the whole ACTION input — for /eval that is
|
|
116
|
-
* `{code, input, timeout_ms, …}`, and only its `input` field is bound as the script's
|
|
117
|
-
* parameter. genroc cannot know that; this file owns the evaluator's wire contract, so
|
|
118
|
-
* the navigation belongs here rather than in genctl. */
|
|
119
|
-
function scriptInput(site, defs) {
|
|
120
|
-
const action = deref(site.input, defs);
|
|
121
|
-
const bound = action?.properties?.input;
|
|
122
|
-
return bound ?? site.input;
|
|
123
|
-
}
|
|
124
100
|
/** Emits one named type per reachable $def rather than inlining: a task output may
|
|
125
101
|
* reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
|
|
126
|
-
function declarations(
|
|
102
|
+
function declarations(at) {
|
|
103
|
+
const { site, where } = at;
|
|
104
|
+
const defs = where.$defs ?? {};
|
|
127
105
|
const used = new Set();
|
|
128
|
-
const input = tsType(
|
|
129
|
-
const output = tsType(site.
|
|
106
|
+
const input = tsType(site.types?.Input, used);
|
|
107
|
+
const output = tsType(site.types?.Output, used);
|
|
130
108
|
const emitted = [];
|
|
131
109
|
const done = new Set();
|
|
132
110
|
while (true) {
|
|
@@ -138,8 +116,8 @@ function declarations(site, defs) {
|
|
|
138
116
|
emitted.push(`export type ${identifier(next)} = ${body};`);
|
|
139
117
|
}
|
|
140
118
|
return [
|
|
141
|
-
"// Generated by genroc. Do not edit - regenerate with `genctl
|
|
142
|
-
`// ${
|
|
119
|
+
"// Generated by genroc. Do not edit - regenerate with `genctl generate`.",
|
|
120
|
+
`// ${where.name} (${address(site.pointer)})`,
|
|
143
121
|
"",
|
|
144
122
|
...emitted,
|
|
145
123
|
emitted.length ? "" : "",
|
|
@@ -155,9 +133,8 @@ function typesPathFor(scriptPath) {
|
|
|
155
133
|
return scriptPath.replace(/\.[^.\/]+$/, "") + ".genroc.d.ts";
|
|
156
134
|
}
|
|
157
135
|
// ── typecheck ──────────────────────────────────────────────────────────────────
|
|
158
|
-
/** The
|
|
159
|
-
*
|
|
160
|
-
* the project root: above it is not this project. */
|
|
136
|
+
/** The tsconfig the author's editor reads, so editor and apply agree. Stops at the project
|
|
137
|
+
* root. */
|
|
161
138
|
async function nearestTsconfig(from, root) {
|
|
162
139
|
for (let dir = from;; dir = dirname(dir)) {
|
|
163
140
|
const candidate = join(dir, "tsconfig.json");
|
|
@@ -167,26 +144,22 @@ async function nearestTsconfig(from, root) {
|
|
|
167
144
|
return null;
|
|
168
145
|
}
|
|
169
146
|
}
|
|
170
|
-
async function typecheck(
|
|
171
|
-
//
|
|
172
|
-
// coexist with it. The suffix is what keeps the scratch area out of its way.
|
|
173
|
-
const dir = join(root, ".genroc-cache");
|
|
174
|
-
await write(join(dir, ".gitignore"), "*\n");
|
|
175
|
-
// One tsc per distinct base config: `extends` takes a single base, so merging two would
|
|
147
|
+
async function typecheck(sites) {
|
|
148
|
+
// One program per distinct base config: `extends` takes a single base, so merging two would
|
|
176
149
|
// check each script under the other author's options.
|
|
177
150
|
const groups = new Map();
|
|
178
|
-
for (const
|
|
179
|
-
const base = (await nearestTsconfig(dirname(
|
|
151
|
+
for (const at of sites) {
|
|
152
|
+
const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
|
|
180
153
|
const group = groups.get(base);
|
|
181
154
|
if (group)
|
|
182
|
-
group.push(
|
|
155
|
+
group.push(at);
|
|
183
156
|
else
|
|
184
|
-
groups.set(base, [
|
|
157
|
+
groups.set(base, [at]);
|
|
185
158
|
}
|
|
186
|
-
|
|
159
|
+
const diagnostics = [];
|
|
187
160
|
for (const [base, group] of groups) {
|
|
188
161
|
const config = {
|
|
189
|
-
...(base ? { extends:
|
|
162
|
+
...(base ? { extends: base } : {}),
|
|
190
163
|
compilerOptions: {
|
|
191
164
|
noEmit: true,
|
|
192
165
|
strict: true,
|
|
@@ -197,42 +170,36 @@ async function typecheck(root, sites) {
|
|
|
197
170
|
// `lib` DESCRIBES the realm and is written after `extends` so a base cannot widen it:
|
|
198
171
|
// a worker thread has no document, whatever an author's config claims.
|
|
199
172
|
lib: ["esnext", "webworker"],
|
|
200
|
-
// `types` is the author's
|
|
201
|
-
//
|
|
202
|
-
// base config there is nothing to opt in with, so the default stays none.
|
|
173
|
+
// `types` is the author's opt-in to node globals, which the realm has. With no base
|
|
174
|
+
// config there is nothing to opt in with, so it stays none.
|
|
203
175
|
...(base ? {} : { types: [] }),
|
|
204
176
|
},
|
|
205
|
-
files: group.flatMap((s) => [
|
|
206
|
-
relative(dir, s.path),
|
|
207
|
-
relative(dir, typesPathFor(s.path)),
|
|
208
|
-
]),
|
|
177
|
+
files: group.flatMap((s) => [s.file, typesPathFor(s.file)]),
|
|
209
178
|
// `files` overrides the base's, but a base `include` survives beside it and would
|
|
210
179
|
// drag the author's whole tree in, to be checked under the worker lib.
|
|
211
180
|
include: [],
|
|
212
181
|
};
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
182
|
+
// Never written; anchors `@types` lookup at the root. Not `tsconfig.json`: that is often the
|
|
183
|
+
// base it extends, and tsc refuses a circular extends.
|
|
184
|
+
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, root, undefined, join(root, "tsconfig.genroc.json"));
|
|
185
|
+
const program = ts.createProgram({
|
|
186
|
+
rootNames: parsed.fileNames,
|
|
187
|
+
options: parsed.options,
|
|
188
|
+
projectReferences: parsed.projectReferences,
|
|
189
|
+
configFileParsingDiagnostics: parsed.errors,
|
|
190
|
+
});
|
|
191
|
+
diagnostics.push(...parsed.errors, ...ts.getPreEmitDiagnostics(program));
|
|
216
192
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
proc.stderr.on("data", (c) => (err += c));
|
|
228
|
-
const code = await new Promise((resolve, reject) => {
|
|
229
|
-
proc.on("error", reject);
|
|
230
|
-
proc.on("close", (c) => resolve(c ?? 1));
|
|
231
|
-
});
|
|
232
|
-
if (code !== 0) {
|
|
233
|
-
// tsc reports on stdout; the exit code IS the type check, so this is the diagnostic
|
|
234
|
-
// genctl surfaces and the reason a failed import never produces a string.
|
|
235
|
-
die([out, err].filter(Boolean).join("\n").trimEnd());
|
|
193
|
+
if (diagnostics.length > 0) {
|
|
194
|
+
// The diagnostics are the whole type check: this is what genctl surfaces, and the reason a
|
|
195
|
+
// failed import never produces a string.
|
|
196
|
+
die(ts
|
|
197
|
+
.formatDiagnostics(diagnostics, {
|
|
198
|
+
getCanonicalFileName: (f) => f,
|
|
199
|
+
getCurrentDirectory: () => root,
|
|
200
|
+
getNewLine: () => "\n",
|
|
201
|
+
})
|
|
202
|
+
.trimEnd());
|
|
236
203
|
}
|
|
237
204
|
}
|
|
238
205
|
// ── bundle ─────────────────────────────────────────────────────────────────────
|
|
@@ -259,11 +226,9 @@ const BUILTIN = new Set([
|
|
|
259
226
|
...builtinModules,
|
|
260
227
|
...builtinModules.map((m) => `node:${m}`),
|
|
261
228
|
]);
|
|
262
|
-
/** Resolves
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* A package resolving to a `.d.ts` is declined — that is a type, not the implementation —
|
|
266
|
-
* which leaves node_modules to nodeResolve. */
|
|
229
|
+
/** Resolves through TYPESCRIPT under the typecheck's config, so a `paths` alias that compiles
|
|
230
|
+
* also bundles. A `.d.ts` resolution is declined (a type, not code), leaving node_modules to
|
|
231
|
+
* nodeResolve. */
|
|
267
232
|
function tsResolve(configPath) {
|
|
268
233
|
let options = {};
|
|
269
234
|
if (configPath) {
|
|
@@ -284,31 +249,29 @@ function tsResolve(configPath) {
|
|
|
284
249
|
},
|
|
285
250
|
};
|
|
286
251
|
}
|
|
287
|
-
/**
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Builtins are
|
|
292
|
-
//
|
|
293
|
-
// of a module that will not be there, which bundles clean and fails at runtime.
|
|
252
|
+
/** A self-contained ES module whose default export is the author's own, unwrapped. Bundling here
|
|
253
|
+
* means a definition version pins its code forever. */
|
|
254
|
+
async function bundle(at) {
|
|
255
|
+
const site = at.site;
|
|
256
|
+
// Builtins are externalised; anything else unresolved is a REFUSAL, since rollup's default
|
|
257
|
+
// leaves an import that bundles clean and fails at runtime.
|
|
294
258
|
const built = await rollup({
|
|
295
|
-
input:
|
|
259
|
+
input: at.file,
|
|
296
260
|
external: (id) => BUILTIN.has(id),
|
|
297
261
|
plugins: [
|
|
298
|
-
tsResolve(await nearestTsconfig(dirname(
|
|
262
|
+
tsResolve(await nearestTsconfig(dirname(at.file), root)),
|
|
299
263
|
nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
|
|
300
264
|
commonjs(),
|
|
301
|
-
//
|
|
302
|
-
// did natively; without it rollup hands the JSON to the JS parser.
|
|
265
|
+
// Inlines `.json` imports; without it rollup hands JSON to the JS parser.
|
|
303
266
|
json(),
|
|
304
267
|
transpile,
|
|
305
268
|
],
|
|
306
269
|
onwarn(warning) {
|
|
307
270
|
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
308
|
-
die(`${
|
|
271
|
+
die(`${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`);
|
|
309
272
|
}
|
|
310
273
|
},
|
|
311
|
-
}).catch((e) => die(`${
|
|
274
|
+
}).catch((e) => die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`));
|
|
312
275
|
const { output } = await built.generate({
|
|
313
276
|
format: "es",
|
|
314
277
|
inlineDynamicImports: true,
|
|
@@ -317,7 +280,7 @@ async function bundle(site, root) {
|
|
|
317
280
|
// Refused here rather than in the realm: the evaluator can only report it against a running
|
|
318
281
|
// instance, and the file it names is on this machine.
|
|
319
282
|
if (!output[0].exports.includes("default")) {
|
|
320
|
-
die(`${
|
|
283
|
+
die(`${at.file}: a script must \`export default\` the function to run`);
|
|
321
284
|
}
|
|
322
285
|
return output[0].code;
|
|
323
286
|
}
|
|
@@ -330,32 +293,69 @@ const stdin = await new Promise((resolve, reject) => {
|
|
|
330
293
|
process.stdin.on("error", reject);
|
|
331
294
|
});
|
|
332
295
|
const manifest = JSON.parse(stdin);
|
|
333
|
-
|
|
296
|
+
// genctl runs a resolver with the project root as its cwd, so the manifest need not say so.
|
|
297
|
+
const root = process.cwd();
|
|
298
|
+
if (!manifest || !Array.isArray(manifest.processes))
|
|
334
299
|
die("stdin is not a genroc resolver manifest");
|
|
300
|
+
/** A pointer as the address it is, so a generated comment or an error can be pasted into
|
|
301
|
+
* `genctl schema`. A key no identifier can spell is bracketed, as the grammar spells it. */
|
|
302
|
+
function address(pointer) {
|
|
303
|
+
return pointer
|
|
304
|
+
.map((seg) => typeof seg === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)
|
|
305
|
+
? `.${seg}`
|
|
306
|
+
: `[${JSON.stringify(seg)}]`)
|
|
307
|
+
.join("")
|
|
308
|
+
.replace(/^\./, "");
|
|
309
|
+
}
|
|
310
|
+
const located = manifest.processes.flatMap((where) => where.sites.map((site) => {
|
|
311
|
+
const [script, ...rest] = site.args;
|
|
312
|
+
if (script === undefined || rest.length > 0) {
|
|
313
|
+
die(`${join(where.dir, where.file)}: ${address(site.pointer)}: $import takes one script, ` +
|
|
314
|
+
`and is given ${site.args.length} arguments`);
|
|
315
|
+
}
|
|
316
|
+
return { site, where, file: resolve(where.dir, script) };
|
|
317
|
+
}));
|
|
318
|
+
// The `code`-field contract is this resolver's, not genctl's, so it checks the directive landed
|
|
319
|
+
// there: in a child call (the scaffold's shape) or an external task's input.
|
|
320
|
+
for (const at of located) {
|
|
321
|
+
// The action's kind is a field; where in it the directive sits is the pointer.
|
|
322
|
+
const kind = at.site.level === "action" ? (at.site.action ?? "") : "";
|
|
323
|
+
const slot = at.site.pointer.slice(-2).join(".");
|
|
324
|
+
if ((kind !== "child" && kind !== "external") || slot !== "input.code") {
|
|
325
|
+
die(`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: an evaluated script ` +
|
|
326
|
+
"belongs in the `code` field of a child or external task's input, and this is " +
|
|
327
|
+
`${kind ? `a ${kind} task's ` : ""}\`${slot}\`.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// genctl does not know the argument is a file, so this resolver reports a missing one.
|
|
331
|
+
for (const at of located) {
|
|
332
|
+
if (!existsSync(at.file)) {
|
|
333
|
+
die(`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: ` +
|
|
334
|
+
`"$import: ${at.site.args[0]}" names no file (looked at ${at.file})`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
335
337
|
// One script at two sites with different input types is a refusal, not a union: the union
|
|
336
338
|
// is sound and would typecheck a body that is wrong at one of the sites.
|
|
337
339
|
const byPath = new Map();
|
|
338
|
-
for (const
|
|
339
|
-
const seen = byPath.get(
|
|
340
|
-
const defsOf = (x) => (manifest.schemas[x.process]?.$defs ?? {});
|
|
340
|
+
for (const at of located) {
|
|
341
|
+
const seen = byPath.get(at.file);
|
|
341
342
|
if (seen &&
|
|
342
|
-
JSON.stringify(
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)) {
|
|
344
|
+
die(`${at.file} is imported at ${address(seen.site.pointer)} and ` +
|
|
345
|
+
`${address(at.site.pointer)} with different input types.\n` +
|
|
345
346
|
"Split it into two scripts, or make the two call sites pass the same shape.");
|
|
346
347
|
}
|
|
347
|
-
byPath.set(
|
|
348
|
+
byPath.set(at.file, at);
|
|
348
349
|
}
|
|
349
|
-
for (const
|
|
350
|
-
|
|
351
|
-
await write(typesPathFor(site.path), declarations(site, defs));
|
|
350
|
+
for (const at of byPath.values()) {
|
|
351
|
+
await writeFile(typesPathFor(at.file), declarations(at));
|
|
352
352
|
}
|
|
353
|
-
if (manifest.mode === "
|
|
353
|
+
if (manifest.mode === "generate") {
|
|
354
354
|
process.exit(0);
|
|
355
355
|
}
|
|
356
|
-
await typecheck(
|
|
357
|
-
const
|
|
358
|
-
for (const
|
|
359
|
-
|
|
356
|
+
await typecheck([...byPath.values()]);
|
|
357
|
+
const values = [];
|
|
358
|
+
for (const at of located) {
|
|
359
|
+
values.push(await bundle(at));
|
|
360
360
|
}
|
|
361
|
-
process.stdout.write(JSON.stringify({
|
|
361
|
+
process.stdout.write(JSON.stringify({ values }));
|
package/dist/realm.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// The evaluation realm
|
|
2
|
-
//
|
|
3
|
-
// eval.ts owns the budget and does the killing; nothing here knows about time.
|
|
4
|
-
//
|
|
5
|
-
// Everything that touches the script's VALUE lives on this side of the boundary — loading,
|
|
6
|
-
// classifying, serialising — because this is the only realm the value exists in.
|
|
1
|
+
// The evaluation realm: one Worker per execution, killable mid-loop by eval.ts, which owns the
|
|
2
|
+
// budget. Everything touching the script's VALUE lives here, the only realm it exists in.
|
|
7
3
|
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
8
4
|
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
9
5
|
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
@@ -14,9 +10,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
14
10
|
};
|
|
15
11
|
import { registerHooks } from "node:module";
|
|
16
12
|
import { parentPort } from "node:worker_threads";
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// the way it does everywhere else.
|
|
13
|
+
// IMPORTED under our own URL rather than compiled from a string, so frames carry the author's
|
|
14
|
+
// line numbers and builtin imports resolve normally.
|
|
20
15
|
const SCRIPT_URL = "script:main";
|
|
21
16
|
const STACK_BYTES = 2_048;
|
|
22
17
|
// The only channel a load hook has to the source. Written per request and read once — a realm
|
|
@@ -41,7 +36,10 @@ function scriptStack(err) {
|
|
|
41
36
|
}
|
|
42
37
|
function describe(err, kind) {
|
|
43
38
|
if (err instanceof Error) {
|
|
44
|
-
|
|
39
|
+
// A subclass inherits `name` from Error.prototype, so `class LimitExceeded extends Error {}`
|
|
40
|
+
// would report "Error"; a name the script set still wins.
|
|
41
|
+
const name = err.name === "Error" && err.constructor.name ? err.constructor.name : err.name;
|
|
42
|
+
return { kind, name, message: err.message, stack: scriptStack(err) };
|
|
45
43
|
}
|
|
46
44
|
// A script may throw a non-Error (`throw {code: "x"}`), so name/message must not assume one.
|
|
47
45
|
return { kind, name: "Thrown", message: safeText(err) };
|
|
@@ -54,9 +52,8 @@ function safeText(v) {
|
|
|
54
52
|
return String(v);
|
|
55
53
|
}
|
|
56
54
|
}
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
* which is the script throwing. */
|
|
55
|
+
/** Unparseable code or an unresolvable import is broken code; anything else the import throws is
|
|
56
|
+
* the module's top level running, i.e. the script throwing. */
|
|
60
57
|
function unloadable(err) {
|
|
61
58
|
const code = err?.code;
|
|
62
59
|
return err instanceof SyntaxError || (typeof code === "string" && code.startsWith("ERR_MODULE"));
|
|
@@ -97,10 +94,8 @@ async function run(req) {
|
|
|
97
94
|
return { ok: false, failure: describe(err, "nonserializable") };
|
|
98
95
|
}
|
|
99
96
|
}
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// write's callback fires once the queue ahead of it has drained, which makes the reply a barrier
|
|
103
|
-
// for `console` and a direct process.stdout.write alike: both are this stream.
|
|
97
|
+
// eval.ts terminates this thread the moment the reply lands, while stdio may still be in the
|
|
98
|
+
// pipe. An empty write's callback fires once the queue drains, making the reply a barrier.
|
|
104
99
|
function flush(stream) {
|
|
105
100
|
return new Promise((resolve) => stream.write("", () => resolve()));
|
|
106
101
|
}
|