@genroc/eval-node 0.0.0-edge.dc4ff88 → 0.0.0-edge.dcfec388
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 +70 -71
- package/eval.ts +30 -21
- package/import.ts +175 -148
- package/package.json +1 -1
- package/realm.ts +12 -17
- package/worker.ts +78 -74
package/import.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The
|
|
3
|
-
// with
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
// over the same project.
|
|
9
|
-
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
|
-
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
|
+
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { access, writeFile } from "node:fs/promises";
|
|
12
8
|
import { builtinModules } from "node:module";
|
|
13
|
-
import { dirname, join,
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
10
|
|
|
16
11
|
import commonjs from "@rollup/plugin-commonjs";
|
|
17
12
|
import json from "@rollup/plugin-json";
|
|
@@ -22,20 +17,36 @@ import ts from "typescript";
|
|
|
22
17
|
type Schema = Record<string, any>;
|
|
23
18
|
|
|
24
19
|
type Site = {
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// Which namespace the directive sits in: `process`, `task` or `action`.
|
|
21
|
+
level: "process" | "task" | "action";
|
|
27
22
|
task?: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
// What the site IS, at action level: the action's type, and the process a child call names.
|
|
24
|
+
action?: string;
|
|
25
|
+
child?: string;
|
|
26
|
+
// The slot as a path of keys and indices, so nothing has to unescape a JSON Pointer.
|
|
27
|
+
pointer: (string | number)[];
|
|
28
|
+
// The argument's words, split as sh splits single quotes. genctl does not read them as paths —
|
|
29
|
+
// that is this resolver's reading of them.
|
|
30
|
+
args: string[];
|
|
31
|
+
// What .genroc asked genctl to type, keyed by the name it chose. This resolver reads Input
|
|
32
|
+
// and Output; which ADDRESS each came from is the config's business, not ours.
|
|
33
|
+
types?: Record<string, Schema>;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
type ManifestProcess = {
|
|
37
|
+
name: string;
|
|
38
|
+
// The definition's own location, split because a relative argument is relative to the
|
|
39
|
+
// DIRECTORY — joining is ours to do, and this is the base.
|
|
40
|
+
dir: string;
|
|
41
|
+
file: string;
|
|
38
42
|
sites: Site[];
|
|
43
|
+
// Only what this process's fragments reach — a `$ref` in `types` points here.
|
|
44
|
+
$defs?: Record<string, Schema>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type Manifest = {
|
|
48
|
+
mode: "generate" | "resolve";
|
|
49
|
+
processes: ManifestProcess[];
|
|
39
50
|
};
|
|
40
51
|
|
|
41
52
|
function die(message: string): never {
|
|
@@ -52,12 +63,6 @@ async function exists(path: string): Promise<boolean> {
|
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
65
|
|
|
55
|
-
/** Creates the parent directory, which `.genroc-cache/` relies on: nothing else makes it. */
|
|
56
|
-
async function write(path: string, content: string): Promise<void> {
|
|
57
|
-
await mkdir(dirname(path), { recursive: true });
|
|
58
|
-
await writeFile(path, content);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
66
|
// ── JSON Schema → TypeScript ───────────────────────────────────────────────────
|
|
62
67
|
|
|
63
68
|
/** Only genroc's keyword set is handled; anything else its strict decoder would have
|
|
@@ -119,16 +124,21 @@ function objectType(s: Schema, used: Set<string>): string {
|
|
|
119
124
|
? ` /** ${sub.description} */\n`
|
|
120
125
|
: "";
|
|
121
126
|
lines.push(
|
|
122
|
-
`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${tsType(sub, used)};`,
|
|
127
|
+
`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${nest(tsType(sub, used))};`,
|
|
123
128
|
);
|
|
124
129
|
}
|
|
125
130
|
if (s.additionalProperties && typeof s.additionalProperties === "object") {
|
|
126
|
-
lines.push(
|
|
131
|
+
lines.push(
|
|
132
|
+
` [key: string]: ${nest(tsType(s.additionalProperties, used))};`,
|
|
133
|
+
);
|
|
127
134
|
}
|
|
128
135
|
if (lines.length === 0) return "Record<string, unknown>";
|
|
129
136
|
return `{\n${lines.join("\n")}\n}`;
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
/** Every line but the first, which is already placed by the property that opens it. */
|
|
140
|
+
const nest = (t: string) => t.replace(/\n/g, "\n ");
|
|
141
|
+
|
|
132
142
|
function union(parts: string[]): string {
|
|
133
143
|
const seen = [...new Set(parts)];
|
|
134
144
|
return seen.length === 0 ? "unknown" : seen.join(" | ");
|
|
@@ -139,36 +149,14 @@ const propKey = (k: string) => (IDENT.test(k) ? k : JSON.stringify(k));
|
|
|
139
149
|
const identifier = (n: string) =>
|
|
140
150
|
IDENT.test(n) ? n : `Def_${n.replace(/[^A-Za-z0-9_$]/g, "_")}`;
|
|
141
151
|
|
|
142
|
-
function deref(
|
|
143
|
-
s: Schema | undefined,
|
|
144
|
-
defs: Record<string, Schema>,
|
|
145
|
-
): Schema | undefined {
|
|
146
|
-
let cur = s;
|
|
147
|
-
for (let i = 0; cur && typeof cur.$ref === "string" && i < 16; i++) {
|
|
148
|
-
cur = defs[cur.$ref.replace(/^#\/\$defs\//, "")];
|
|
149
|
-
}
|
|
150
|
-
return cur;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** The manifest's `input` is the type of the whole ACTION input — for /eval that is
|
|
154
|
-
* `{code, input, timeout_ms, …}`, and only its `input` field is bound as the script's
|
|
155
|
-
* parameter. genroc cannot know that; this file owns the evaluator's wire contract, so
|
|
156
|
-
* the navigation belongs here rather than in genctl. */
|
|
157
|
-
function scriptInput(
|
|
158
|
-
site: Site,
|
|
159
|
-
defs: Record<string, Schema>,
|
|
160
|
-
): Schema | undefined {
|
|
161
|
-
const action = deref(site.input, defs);
|
|
162
|
-
const bound = action?.properties?.input;
|
|
163
|
-
return bound ?? site.input;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
152
|
/** Emits one named type per reachable $def rather than inlining: a task output may
|
|
167
153
|
* reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
|
|
168
|
-
function declarations(
|
|
154
|
+
function declarations(at: Located): string {
|
|
155
|
+
const { site, where } = at;
|
|
156
|
+
const defs = where.$defs ?? {};
|
|
169
157
|
const used = new Set<string>();
|
|
170
|
-
const input = tsType(
|
|
171
|
-
const output = tsType(site.
|
|
158
|
+
const input = tsType(site.types?.Input, used);
|
|
159
|
+
const output = tsType(site.types?.Output, used);
|
|
172
160
|
|
|
173
161
|
const emitted: string[] = [];
|
|
174
162
|
const done = new Set<string>();
|
|
@@ -181,8 +169,8 @@ function declarations(site: Site, defs: Record<string, Schema>): string {
|
|
|
181
169
|
}
|
|
182
170
|
|
|
183
171
|
return [
|
|
184
|
-
"// Generated by genroc. Do not edit - regenerate with `genctl
|
|
185
|
-
`// ${
|
|
172
|
+
"// Generated by genroc. Do not edit - regenerate with `genctl generate`.",
|
|
173
|
+
`// ${where.name} (${address(site.pointer)})`,
|
|
186
174
|
"",
|
|
187
175
|
...emitted,
|
|
188
176
|
emitted.length ? "" : "",
|
|
@@ -201,9 +189,8 @@ function typesPathFor(scriptPath: string): string {
|
|
|
201
189
|
|
|
202
190
|
// ── typecheck ──────────────────────────────────────────────────────────────────
|
|
203
191
|
|
|
204
|
-
/** The
|
|
205
|
-
*
|
|
206
|
-
* the project root: above it is not this project. */
|
|
192
|
+
/** The tsconfig the author's editor reads, so editor and apply agree. Stops at the project
|
|
193
|
+
* root. */
|
|
207
194
|
async function nearestTsconfig(
|
|
208
195
|
from: string,
|
|
209
196
|
root: string,
|
|
@@ -215,26 +202,21 @@ async function nearestTsconfig(
|
|
|
215
202
|
}
|
|
216
203
|
}
|
|
217
204
|
|
|
218
|
-
async function typecheck(
|
|
219
|
-
//
|
|
220
|
-
// coexist with it. The suffix is what keeps the scratch area out of its way.
|
|
221
|
-
const dir = join(root, ".genroc-cache");
|
|
222
|
-
await write(join(dir, ".gitignore"), "*\n");
|
|
223
|
-
|
|
224
|
-
// One tsc per distinct base config: `extends` takes a single base, so merging two would
|
|
205
|
+
async function typecheck(sites: Located[]): Promise<void> {
|
|
206
|
+
// One program per distinct base config: `extends` takes a single base, so merging two would
|
|
225
207
|
// check each script under the other author's options.
|
|
226
|
-
const groups = new Map<string,
|
|
227
|
-
for (const
|
|
228
|
-
const base = (await nearestTsconfig(dirname(
|
|
208
|
+
const groups = new Map<string, Located[]>();
|
|
209
|
+
for (const at of sites) {
|
|
210
|
+
const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
|
|
229
211
|
const group = groups.get(base);
|
|
230
|
-
if (group) group.push(
|
|
231
|
-
else groups.set(base, [
|
|
212
|
+
if (group) group.push(at);
|
|
213
|
+
else groups.set(base, [at]);
|
|
232
214
|
}
|
|
233
215
|
|
|
234
|
-
|
|
216
|
+
const diagnostics: ts.Diagnostic[] = [];
|
|
235
217
|
for (const [base, group] of groups) {
|
|
236
218
|
const config: Record<string, unknown> = {
|
|
237
|
-
...(base ? { extends:
|
|
219
|
+
...(base ? { extends: base } : {}),
|
|
238
220
|
compilerOptions: {
|
|
239
221
|
noEmit: true,
|
|
240
222
|
strict: true,
|
|
@@ -245,46 +227,45 @@ async function typecheck(root: string, sites: Site[]): Promise<void> {
|
|
|
245
227
|
// `lib` DESCRIBES the realm and is written after `extends` so a base cannot widen it:
|
|
246
228
|
// a worker thread has no document, whatever an author's config claims.
|
|
247
229
|
lib: ["esnext", "webworker"],
|
|
248
|
-
// `types` is the author's
|
|
249
|
-
//
|
|
250
|
-
// base config there is nothing to opt in with, so the default stays none.
|
|
230
|
+
// `types` is the author's opt-in to node globals, which the realm has. With no base
|
|
231
|
+
// config there is nothing to opt in with, so it stays none.
|
|
251
232
|
...(base ? {} : { types: [] }),
|
|
252
233
|
},
|
|
253
|
-
files: group.flatMap((s) => [
|
|
254
|
-
relative(dir, s.path),
|
|
255
|
-
relative(dir, typesPathFor(s.path)),
|
|
256
|
-
]),
|
|
234
|
+
files: group.flatMap((s) => [s.file, typesPathFor(s.file)]),
|
|
257
235
|
// `files` overrides the base's, but a base `include` survives beside it and would
|
|
258
236
|
// drag the author's whole tree in, to be checked under the worker lib.
|
|
259
237
|
include: [],
|
|
260
238
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
239
|
+
// Never written; anchors `@types` lookup at the root. Not `tsconfig.json`: that is often the
|
|
240
|
+
// base it extends, and tsc refuses a circular extends.
|
|
241
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
242
|
+
config,
|
|
243
|
+
ts.sys,
|
|
244
|
+
root,
|
|
245
|
+
undefined,
|
|
246
|
+
join(root, "tsconfig.genroc.json"),
|
|
264
247
|
);
|
|
265
|
-
|
|
266
|
-
|
|
248
|
+
const program = ts.createProgram({
|
|
249
|
+
rootNames: parsed.fileNames,
|
|
250
|
+
options: parsed.options,
|
|
251
|
+
projectReferences: parsed.projectReferences,
|
|
252
|
+
configFileParsingDiagnostics: parsed.errors,
|
|
253
|
+
});
|
|
254
|
+
diagnostics.push(...parsed.errors, ...ts.getPreEmitDiagnostics(program));
|
|
267
255
|
}
|
|
268
|
-
}
|
|
269
256
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
proc.on("close", (c) => resolve(c ?? 1));
|
|
283
|
-
});
|
|
284
|
-
if (code !== 0) {
|
|
285
|
-
// tsc reports on stdout; the exit code IS the type check, so this is the diagnostic
|
|
286
|
-
// genctl surfaces and the reason a failed import never produces a string.
|
|
287
|
-
die([out, err].filter(Boolean).join("\n").trimEnd());
|
|
257
|
+
if (diagnostics.length > 0) {
|
|
258
|
+
// The diagnostics are the whole type check: this is what genctl surfaces, and the reason a
|
|
259
|
+
// failed import never produces a string.
|
|
260
|
+
die(
|
|
261
|
+
ts
|
|
262
|
+
.formatDiagnostics(diagnostics, {
|
|
263
|
+
getCanonicalFileName: (f) => f,
|
|
264
|
+
getCurrentDirectory: () => root,
|
|
265
|
+
getNewLine: () => "\n",
|
|
266
|
+
})
|
|
267
|
+
.trimEnd(),
|
|
268
|
+
);
|
|
288
269
|
}
|
|
289
270
|
}
|
|
290
271
|
|
|
@@ -314,11 +295,9 @@ const BUILTIN = new Set([
|
|
|
314
295
|
...builtinModules.map((m) => `node:${m}`),
|
|
315
296
|
]);
|
|
316
297
|
|
|
317
|
-
/** Resolves
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* A package resolving to a `.d.ts` is declined — that is a type, not the implementation —
|
|
321
|
-
* which leaves node_modules to nodeResolve. */
|
|
298
|
+
/** Resolves through TYPESCRIPT under the typecheck's config, so a `paths` alias that compiles
|
|
299
|
+
* also bundles. A `.d.ts` resolution is declined (a type, not code), leaving node_modules to
|
|
300
|
+
* nodeResolve. */
|
|
322
301
|
function tsResolve(configPath: string | null): Plugin {
|
|
323
302
|
let options: ts.CompilerOptions = {};
|
|
324
303
|
if (configPath) {
|
|
@@ -348,34 +327,32 @@ function tsResolve(configPath: string | null): Plugin {
|
|
|
348
327
|
};
|
|
349
328
|
}
|
|
350
329
|
|
|
351
|
-
/**
|
|
352
|
-
*
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// Builtins are
|
|
356
|
-
//
|
|
357
|
-
// of a module that will not be there, which bundles clean and fails at runtime.
|
|
330
|
+
/** A self-contained ES module whose default export is the author's own, unwrapped. Bundling here
|
|
331
|
+
* means a definition version pins its code forever. */
|
|
332
|
+
async function bundle(at: Located): Promise<string> {
|
|
333
|
+
const site = at.site;
|
|
334
|
+
// Builtins are externalised; anything else unresolved is a REFUSAL, since rollup's default
|
|
335
|
+
// leaves an import that bundles clean and fails at runtime.
|
|
358
336
|
const built = await rollup({
|
|
359
|
-
input:
|
|
337
|
+
input: at.file,
|
|
360
338
|
external: (id) => BUILTIN.has(id),
|
|
361
339
|
plugins: [
|
|
362
|
-
tsResolve(await nearestTsconfig(dirname(
|
|
340
|
+
tsResolve(await nearestTsconfig(dirname(at.file), root)),
|
|
363
341
|
nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
|
|
364
342
|
commonjs(),
|
|
365
|
-
//
|
|
366
|
-
// did natively; without it rollup hands the JSON to the JS parser.
|
|
343
|
+
// Inlines `.json` imports; without it rollup hands JSON to the JS parser.
|
|
367
344
|
json(),
|
|
368
345
|
transpile,
|
|
369
346
|
],
|
|
370
347
|
onwarn(warning) {
|
|
371
348
|
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
372
349
|
die(
|
|
373
|
-
`${
|
|
350
|
+
`${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`,
|
|
374
351
|
);
|
|
375
352
|
}
|
|
376
353
|
},
|
|
377
354
|
}).catch((e: unknown) =>
|
|
378
|
-
die(`${
|
|
355
|
+
die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`),
|
|
379
356
|
);
|
|
380
357
|
|
|
381
358
|
const { output } = await built.generate({
|
|
@@ -386,7 +363,7 @@ async function bundle(site: Site, root: string): Promise<string> {
|
|
|
386
363
|
// Refused here rather than in the realm: the evaluator can only report it against a running
|
|
387
364
|
// instance, and the file it names is on this machine.
|
|
388
365
|
if (!output[0].exports.includes("default")) {
|
|
389
|
-
die(`${
|
|
366
|
+
die(`${at.file}: a script must \`export default\` the function to run`);
|
|
390
367
|
}
|
|
391
368
|
return output[0].code;
|
|
392
369
|
}
|
|
@@ -401,45 +378,95 @@ const stdin: string = await new Promise((resolve, reject) => {
|
|
|
401
378
|
process.stdin.on("error", reject);
|
|
402
379
|
});
|
|
403
380
|
const manifest = JSON.parse(stdin) as Manifest;
|
|
404
|
-
|
|
381
|
+
// genctl runs a resolver with the project root as its cwd, so the manifest need not say so.
|
|
382
|
+
const root = process.cwd();
|
|
383
|
+
if (!manifest || !Array.isArray(manifest.processes))
|
|
405
384
|
die("stdin is not a genroc resolver manifest");
|
|
406
385
|
|
|
386
|
+
/** Sites in manifest order, the order `code` must answer in. The argument is joined to the
|
|
387
|
+
* definition's directory here: only this resolver knows it is a path. */
|
|
388
|
+
type Located = { site: Site; where: ManifestProcess; file: string };
|
|
389
|
+
|
|
390
|
+
/** A pointer as the address it is, so a generated comment or an error can be pasted into
|
|
391
|
+
* `genctl schema`. A key no identifier can spell is bracketed, as the grammar spells it. */
|
|
392
|
+
function address(pointer: (string | number)[]): string {
|
|
393
|
+
return pointer
|
|
394
|
+
.map((seg) =>
|
|
395
|
+
typeof seg === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)
|
|
396
|
+
? `.${seg}`
|
|
397
|
+
: `[${JSON.stringify(seg)}]`,
|
|
398
|
+
)
|
|
399
|
+
.join("")
|
|
400
|
+
.replace(/^\./, "");
|
|
401
|
+
}
|
|
402
|
+
const located: Located[] = manifest.processes.flatMap((where) =>
|
|
403
|
+
where.sites.map((site) => {
|
|
404
|
+
const [script, ...rest] = site.args;
|
|
405
|
+
if (script === undefined || rest.length > 0) {
|
|
406
|
+
die(
|
|
407
|
+
`${join(where.dir, where.file)}: ${address(site.pointer)}: $import takes one script, ` +
|
|
408
|
+
`and is given ${site.args.length} arguments`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
return { site, where, file: resolve(where.dir, script) };
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// The `code`-field contract is this resolver's, not genctl's, so it checks the directive landed
|
|
416
|
+
// there: in a child call (the scaffold's shape) or an external task's input.
|
|
417
|
+
for (const at of located) {
|
|
418
|
+
// The action's kind is a field; where in it the directive sits is the pointer.
|
|
419
|
+
const kind = at.site.level === "action" ? (at.site.action ?? "") : "";
|
|
420
|
+
const slot = at.site.pointer.slice(-2).join(".");
|
|
421
|
+
if ((kind !== "child" && kind !== "external") || slot !== "input.code") {
|
|
422
|
+
die(
|
|
423
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: an evaluated script ` +
|
|
424
|
+
"belongs in the `code` field of a child or external task's input, and this is " +
|
|
425
|
+
`${kind ? `a ${kind} task's ` : ""}\`${slot}\`.`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// genctl does not know the argument is a file, so this resolver reports a missing one.
|
|
431
|
+
for (const at of located) {
|
|
432
|
+
if (!existsSync(at.file)) {
|
|
433
|
+
die(
|
|
434
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: ` +
|
|
435
|
+
`"$import: ${at.site.args[0]}" names no file (looked at ${at.file})`,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
407
440
|
// One script at two sites with different input types is a refusal, not a union: the union
|
|
408
441
|
// is sound and would typecheck a body that is wrong at one of the sites.
|
|
409
|
-
const byPath = new Map<string,
|
|
410
|
-
for (const
|
|
411
|
-
const seen = byPath.get(
|
|
412
|
-
const defsOf = (x: Site) =>
|
|
413
|
-
(manifest.schemas[x.process]?.$defs ?? {}) as Record<string, Schema>;
|
|
442
|
+
const byPath = new Map<string, Located>();
|
|
443
|
+
for (const at of located) {
|
|
444
|
+
const seen = byPath.get(at.file);
|
|
414
445
|
if (
|
|
415
446
|
seen &&
|
|
416
|
-
JSON.stringify(
|
|
417
|
-
JSON.stringify(scriptInput(site, defsOf(site)))
|
|
447
|
+
JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)
|
|
418
448
|
) {
|
|
419
449
|
die(
|
|
420
|
-
`${
|
|
450
|
+
`${at.file} is imported at ${address(seen.site.pointer)} and ` +
|
|
451
|
+
`${address(at.site.pointer)} with different input types.\n` +
|
|
421
452
|
"Split it into two scripts, or make the two call sites pass the same shape.",
|
|
422
453
|
);
|
|
423
454
|
}
|
|
424
|
-
byPath.set(
|
|
455
|
+
byPath.set(at.file, at);
|
|
425
456
|
}
|
|
426
457
|
|
|
427
|
-
for (const
|
|
428
|
-
|
|
429
|
-
string,
|
|
430
|
-
Schema
|
|
431
|
-
>;
|
|
432
|
-
await write(typesPathFor(site.path), declarations(site, defs));
|
|
458
|
+
for (const at of byPath.values()) {
|
|
459
|
+
await writeFile(typesPathFor(at.file), declarations(at));
|
|
433
460
|
}
|
|
434
461
|
|
|
435
|
-
if (manifest.mode === "
|
|
462
|
+
if (manifest.mode === "generate") {
|
|
436
463
|
process.exit(0);
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
await typecheck(
|
|
466
|
+
await typecheck([...byPath.values()]);
|
|
440
467
|
|
|
441
|
-
const
|
|
442
|
-
for (const
|
|
443
|
-
|
|
468
|
+
const values: string[] = [];
|
|
469
|
+
for (const at of located) {
|
|
470
|
+
values.push(await bundle(at));
|
|
444
471
|
}
|
|
445
|
-
process.stdout.write(JSON.stringify({
|
|
472
|
+
process.stdout.write(JSON.stringify({ values }));
|
package/package.json
CHANGED
package/realm.ts
CHANGED
|
@@ -1,18 +1,13 @@
|
|
|
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
|
|
|
8
4
|
import { registerHooks } from "node:module";
|
|
9
5
|
import { parentPort } from "node:worker_threads";
|
|
10
6
|
|
|
11
7
|
import type { EvalFailure, FailureKind, WorkerReply, WorkerRequest } from "./eval.ts";
|
|
12
8
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// the way it does everywhere else.
|
|
9
|
+
// IMPORTED under our own URL rather than compiled from a string, so frames carry the author's
|
|
10
|
+
// line numbers and builtin imports resolve normally.
|
|
16
11
|
const SCRIPT_URL = "script:main";
|
|
17
12
|
const STACK_BYTES = 2_048;
|
|
18
13
|
|
|
@@ -40,7 +35,10 @@ function scriptStack(err: unknown): string | undefined {
|
|
|
40
35
|
|
|
41
36
|
function describe(err: unknown, kind: FailureKind): EvalFailure {
|
|
42
37
|
if (err instanceof Error) {
|
|
43
|
-
|
|
38
|
+
// A subclass inherits `name` from Error.prototype, so `class LimitExceeded extends Error {}`
|
|
39
|
+
// would report "Error"; a name the script set still wins.
|
|
40
|
+
const name = err.name === "Error" && err.constructor.name ? err.constructor.name : err.name;
|
|
41
|
+
return { kind, name, message: err.message, stack: scriptStack(err) };
|
|
44
42
|
}
|
|
45
43
|
// A script may throw a non-Error (`throw {code: "x"}`), so name/message must not assume one.
|
|
46
44
|
return { kind, name: "Thrown", message: safeText(err) };
|
|
@@ -54,9 +52,8 @@ function safeText(v: unknown): string {
|
|
|
54
52
|
}
|
|
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: unknown): boolean {
|
|
61
58
|
const code = (err as { code?: unknown } | null)?.code;
|
|
62
59
|
return err instanceof SyntaxError || (typeof code === "string" && code.startsWith("ERR_MODULE"));
|
|
@@ -99,10 +96,8 @@ async function run(req: WorkerRequest): Promise<WorkerReply> {
|
|
|
99
96
|
}
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
// write's callback fires once the queue ahead of it has drained, which makes the reply a barrier
|
|
105
|
-
// for `console` and a direct process.stdout.write alike: both are this stream.
|
|
99
|
+
// eval.ts terminates this thread the moment the reply lands, while stdio may still be in the
|
|
100
|
+
// pipe. An empty write's callback fires once the queue drains, making the reply a barrier.
|
|
106
101
|
function flush(stream: NodeJS.WriteStream): Promise<void> {
|
|
107
102
|
return new Promise((resolve) => stream.write("", () => resolve()));
|
|
108
103
|
}
|