@genroc/eval-node 0.0.0-edge.3f08782 → 0.0.0-edge.3fd9037f
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 +58 -34
- package/dist/eval.js +25 -4
- package/dist/import.js +130 -105
- package/dist/realm.js +66 -75
- package/dist/worker.js +39 -14
- package/eval.ts +25 -4
- package/import.ts +217 -126
- package/package.json +1 -1
- package/realm.ts +61 -77
- package/worker.ts +48 -17
package/import.ts
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
// code; "build" typechecks and bundles. A separate types hook would mean a second `tsc`
|
|
8
8
|
// over the same project.
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { access,
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { access, writeFile } from "node:fs/promises";
|
|
12
12
|
import { builtinModules } from "node:module";
|
|
13
|
-
import { dirname, join,
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
14
|
|
|
16
15
|
import commonjs from "@rollup/plugin-commonjs";
|
|
17
16
|
import json from "@rollup/plugin-json";
|
|
@@ -22,20 +21,37 @@ import ts from "typescript";
|
|
|
22
21
|
type Schema = Record<string, any>;
|
|
23
22
|
|
|
24
23
|
type Site = {
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// Which namespace the directive sits in: `process`, `task` or `action`.
|
|
25
|
+
level: "process" | "task" | "action";
|
|
27
26
|
task?: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// What the site IS, at action level: the action's type, and the process a child call names.
|
|
28
|
+
action?: string;
|
|
29
|
+
child?: string;
|
|
30
|
+
// The slot as a path of keys and indices, so nothing has to unescape a JSON Pointer.
|
|
31
|
+
pointer: (string | number)[];
|
|
32
|
+
// Everything after `$<resolver>:`, verbatim. genctl does not read it as a path — that is
|
|
33
|
+
// this resolver's reading of it.
|
|
34
|
+
argument: string;
|
|
35
|
+
// What .genroc asked genctl to type, keyed by the name it chose. This resolver reads Input
|
|
36
|
+
// and Output; which ADDRESS each came from is the config's business, not ours.
|
|
37
|
+
types?: Record<string, Schema>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type ManifestProcess = {
|
|
41
|
+
name: string;
|
|
42
|
+
// The definition's own location, split because a relative argument is relative to the
|
|
43
|
+
// DIRECTORY — joining is ours to do, and this is the base.
|
|
44
|
+
dir: string;
|
|
45
|
+
file: string;
|
|
46
|
+
sites: Site[];
|
|
47
|
+
// Only what this process's fragments reach — a `$ref` in `types` points here.
|
|
48
|
+
$defs?: Record<string, Schema>;
|
|
32
49
|
};
|
|
33
50
|
|
|
34
51
|
type Manifest = {
|
|
35
52
|
mode: "types" | "build";
|
|
36
53
|
root: string;
|
|
37
|
-
|
|
38
|
-
sites: Site[];
|
|
54
|
+
processes: ManifestProcess[];
|
|
39
55
|
};
|
|
40
56
|
|
|
41
57
|
function die(message: string): never {
|
|
@@ -52,12 +68,6 @@ async function exists(path: string): Promise<boolean> {
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
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
71
|
// ── JSON Schema → TypeScript ───────────────────────────────────────────────────
|
|
62
72
|
|
|
63
73
|
/** Only genroc's keyword set is handled; anything else its strict decoder would have
|
|
@@ -72,13 +82,16 @@ function tsType(s: Schema | undefined, used: Set<string>): string {
|
|
|
72
82
|
if (Array.isArray(s.enum)) {
|
|
73
83
|
return s.enum.map((v: unknown) => JSON.stringify(v)).join(" | ") || "never";
|
|
74
84
|
}
|
|
75
|
-
if (Array.isArray(s.anyOf))
|
|
76
|
-
|
|
85
|
+
if (Array.isArray(s.anyOf))
|
|
86
|
+
return union(s.anyOf.map((a: Schema) => tsType(a, used)));
|
|
87
|
+
if (Array.isArray(s.oneOf))
|
|
88
|
+
return union(s.oneOf.map((a: Schema) => tsType(a, used)));
|
|
77
89
|
if (Array.isArray(s.allOf)) {
|
|
78
90
|
return s.allOf.map((a: Schema) => tsType(a, used)).join(" & ") || "unknown";
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
const types: string[] =
|
|
93
|
+
const types: string[] =
|
|
94
|
+
s.type === undefined ? [] : Array.isArray(s.type) ? s.type : [s.type];
|
|
82
95
|
if (types.length === 0) {
|
|
83
96
|
// The top type: `{}` means unknown, not "an empty object". specs/unknown-type.md.
|
|
84
97
|
return s.properties ? objectType(s, used) : "unknown";
|
|
@@ -111,16 +124,26 @@ function objectType(s: Schema, used: Set<string>): string {
|
|
|
111
124
|
const required = new Set<string>(s.required ?? []);
|
|
112
125
|
const lines: string[] = [];
|
|
113
126
|
for (const [key, sub] of Object.entries(props)) {
|
|
114
|
-
const doc =
|
|
115
|
-
|
|
127
|
+
const doc =
|
|
128
|
+
typeof sub.description === "string"
|
|
129
|
+
? ` /** ${sub.description} */\n`
|
|
130
|
+
: "";
|
|
131
|
+
lines.push(
|
|
132
|
+
`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${nest(tsType(sub, used))};`,
|
|
133
|
+
);
|
|
116
134
|
}
|
|
117
135
|
if (s.additionalProperties && typeof s.additionalProperties === "object") {
|
|
118
|
-
lines.push(
|
|
136
|
+
lines.push(
|
|
137
|
+
` [key: string]: ${nest(tsType(s.additionalProperties, used))};`,
|
|
138
|
+
);
|
|
119
139
|
}
|
|
120
140
|
if (lines.length === 0) return "Record<string, unknown>";
|
|
121
141
|
return `{\n${lines.join("\n")}\n}`;
|
|
122
142
|
}
|
|
123
143
|
|
|
144
|
+
/** Every line but the first, which is already placed by the property that opens it. */
|
|
145
|
+
const nest = (t: string) => t.replace(/\n/g, "\n ");
|
|
146
|
+
|
|
124
147
|
function union(parts: string[]): string {
|
|
125
148
|
const seen = [...new Set(parts)];
|
|
126
149
|
return seen.length === 0 ? "unknown" : seen.join(" | ");
|
|
@@ -128,32 +151,17 @@ function union(parts: string[]): string {
|
|
|
128
151
|
|
|
129
152
|
const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
130
153
|
const propKey = (k: string) => (IDENT.test(k) ? k : JSON.stringify(k));
|
|
131
|
-
const identifier = (n: string) =>
|
|
132
|
-
|
|
133
|
-
function deref(s: Schema | undefined, defs: Record<string, Schema>): Schema | undefined {
|
|
134
|
-
let cur = s;
|
|
135
|
-
for (let i = 0; cur && typeof cur.$ref === "string" && i < 16; i++) {
|
|
136
|
-
cur = defs[cur.$ref.replace(/^#\/\$defs\//, "")];
|
|
137
|
-
}
|
|
138
|
-
return cur;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** The manifest's `input` is the type of the whole ACTION input — for /eval that is
|
|
142
|
-
* `{code, input, timeout_ms, …}`, and only its `input` field is bound as the script's
|
|
143
|
-
* parameter. genroc cannot know that; this file owns the evaluator's wire contract, so
|
|
144
|
-
* the navigation belongs here rather than in genctl. */
|
|
145
|
-
function scriptInput(site: Site, defs: Record<string, Schema>): Schema | undefined {
|
|
146
|
-
const action = deref(site.input, defs);
|
|
147
|
-
const bound = action?.properties?.input;
|
|
148
|
-
return bound ?? site.input;
|
|
149
|
-
}
|
|
154
|
+
const identifier = (n: string) =>
|
|
155
|
+
IDENT.test(n) ? n : `Def_${n.replace(/[^A-Za-z0-9_$]/g, "_")}`;
|
|
150
156
|
|
|
151
157
|
/** Emits one named type per reachable $def rather than inlining: a task output may
|
|
152
158
|
* reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
|
|
153
|
-
function declarations(
|
|
159
|
+
function declarations(at: Located): string {
|
|
160
|
+
const { site, where } = at;
|
|
161
|
+
const defs = where.$defs ?? {};
|
|
154
162
|
const used = new Set<string>();
|
|
155
|
-
const input = tsType(
|
|
156
|
-
const output = tsType(site.
|
|
163
|
+
const input = tsType(site.types?.Input, used);
|
|
164
|
+
const output = tsType(site.types?.Output, used);
|
|
157
165
|
|
|
158
166
|
const emitted: string[] = [];
|
|
159
167
|
const done = new Set<string>();
|
|
@@ -167,7 +175,7 @@ function declarations(site: Site, defs: Record<string, Schema>): string {
|
|
|
167
175
|
|
|
168
176
|
return [
|
|
169
177
|
"// Generated by genroc. Do not edit - regenerate with `genctl types`.",
|
|
170
|
-
`// ${
|
|
178
|
+
`// ${where.name} (${address(site.pointer)})`,
|
|
171
179
|
"",
|
|
172
180
|
...emitted,
|
|
173
181
|
emitted.length ? "" : "",
|
|
@@ -189,7 +197,10 @@ function typesPathFor(scriptPath: string): string {
|
|
|
189
197
|
/** The nearest tsconfig above the script — the one the author's editor already reads. Two
|
|
190
198
|
* different configs mean a red editor over a clean apply, or the reverse. The walk stops at
|
|
191
199
|
* the project root: above it is not this project. */
|
|
192
|
-
async function nearestTsconfig(
|
|
200
|
+
async function nearestTsconfig(
|
|
201
|
+
from: string,
|
|
202
|
+
root: string,
|
|
203
|
+
): Promise<string | null> {
|
|
193
204
|
for (let dir = from; ; dir = dirname(dir)) {
|
|
194
205
|
const candidate = join(dir, "tsconfig.json");
|
|
195
206
|
if (await exists(candidate)) return candidate;
|
|
@@ -197,26 +208,21 @@ async function nearestTsconfig(from: string, root: string): Promise<string | nul
|
|
|
197
208
|
}
|
|
198
209
|
}
|
|
199
210
|
|
|
200
|
-
async function typecheck(
|
|
201
|
-
//
|
|
202
|
-
// coexist with it. The suffix is what keeps the scratch area out of its way.
|
|
203
|
-
const dir = join(root, ".genroc-cache");
|
|
204
|
-
await write(join(dir, ".gitignore"), "*\n");
|
|
205
|
-
|
|
206
|
-
// One tsc per distinct base config: `extends` takes a single base, so merging two would
|
|
211
|
+
async function typecheck(sites: Located[]): Promise<void> {
|
|
212
|
+
// One program per distinct base config: `extends` takes a single base, so merging two would
|
|
207
213
|
// check each script under the other author's options.
|
|
208
|
-
const groups = new Map<string,
|
|
209
|
-
for (const
|
|
210
|
-
const base = (await nearestTsconfig(dirname(
|
|
214
|
+
const groups = new Map<string, Located[]>();
|
|
215
|
+
for (const at of sites) {
|
|
216
|
+
const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
|
|
211
217
|
const group = groups.get(base);
|
|
212
|
-
if (group) group.push(
|
|
213
|
-
else groups.set(base, [
|
|
218
|
+
if (group) group.push(at);
|
|
219
|
+
else groups.set(base, [at]);
|
|
214
220
|
}
|
|
215
221
|
|
|
216
|
-
|
|
222
|
+
const diagnostics: ts.Diagnostic[] = [];
|
|
217
223
|
for (const [base, group] of groups) {
|
|
218
224
|
const config: Record<string, unknown> = {
|
|
219
|
-
...(base ? { extends:
|
|
225
|
+
...(base ? { extends: base } : {}),
|
|
220
226
|
compilerOptions: {
|
|
221
227
|
noEmit: true,
|
|
222
228
|
strict: true,
|
|
@@ -232,35 +238,42 @@ async function typecheck(root: string, sites: Site[]): Promise<void> {
|
|
|
232
238
|
// base config there is nothing to opt in with, so the default stays none.
|
|
233
239
|
...(base ? {} : { types: [] }),
|
|
234
240
|
},
|
|
235
|
-
files: group.flatMap((s) => [
|
|
241
|
+
files: group.flatMap((s) => [s.file, typesPathFor(s.file)]),
|
|
236
242
|
// `files` overrides the base's, but a base `include` survives beside it and would
|
|
237
243
|
// drag the author's whole tree in, to be checked under the worker lib.
|
|
238
244
|
include: [],
|
|
239
245
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
// Never written: the name only anchors `@types` lookup at the root. It must not be
|
|
247
|
+
// `tsconfig.json`: that is the base the config `extends` whenever the author keeps one at
|
|
248
|
+
// the root, and tsc refuses a config that extends its own path as circular.
|
|
249
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
250
|
+
config,
|
|
251
|
+
ts.sys,
|
|
252
|
+
root,
|
|
253
|
+
undefined,
|
|
254
|
+
join(root, "tsconfig.genroc.json"),
|
|
255
|
+
);
|
|
256
|
+
const program = ts.createProgram({
|
|
257
|
+
rootNames: parsed.fileNames,
|
|
258
|
+
options: parsed.options,
|
|
259
|
+
projectReferences: parsed.projectReferences,
|
|
260
|
+
configFileParsingDiagnostics: parsed.errors,
|
|
261
|
+
});
|
|
262
|
+
diagnostics.push(...parsed.errors, ...ts.getPreEmitDiagnostics(program));
|
|
243
263
|
}
|
|
244
|
-
}
|
|
245
264
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
proc.on("close", (c) => resolve(c ?? 1));
|
|
259
|
-
});
|
|
260
|
-
if (code !== 0) {
|
|
261
|
-
// tsc reports on stdout; the exit code IS the type check, so this is the diagnostic
|
|
262
|
-
// genctl surfaces and the reason a failed import never produces a string.
|
|
263
|
-
die([out, err].filter(Boolean).join("\n").trimEnd());
|
|
265
|
+
if (diagnostics.length > 0) {
|
|
266
|
+
// The diagnostics are the whole type check: this is what genctl surfaces, and the reason a
|
|
267
|
+
// failed import never produces a string.
|
|
268
|
+
die(
|
|
269
|
+
ts
|
|
270
|
+
.formatDiagnostics(diagnostics, {
|
|
271
|
+
getCanonicalFileName: (f) => f,
|
|
272
|
+
getCurrentDirectory: () => root,
|
|
273
|
+
getNewLine: () => "\n",
|
|
274
|
+
})
|
|
275
|
+
.trimEnd(),
|
|
276
|
+
);
|
|
264
277
|
}
|
|
265
278
|
}
|
|
266
279
|
|
|
@@ -285,7 +298,10 @@ const transpile: Plugin = {
|
|
|
285
298
|
},
|
|
286
299
|
};
|
|
287
300
|
|
|
288
|
-
const BUILTIN = new Set([
|
|
301
|
+
const BUILTIN = new Set([
|
|
302
|
+
...builtinModules,
|
|
303
|
+
...builtinModules.map((m) => `node:${m}`),
|
|
304
|
+
]);
|
|
289
305
|
|
|
290
306
|
/** Resolves imports through TYPESCRIPT, using the same config the typecheck ran under, so a
|
|
291
307
|
* `paths` alias that compiles also bundles. Reimplementing `paths` here would be a second
|
|
@@ -296,31 +312,44 @@ function tsResolve(configPath: string | null): Plugin {
|
|
|
296
312
|
let options: ts.CompilerOptions = {};
|
|
297
313
|
if (configPath) {
|
|
298
314
|
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
299
|
-
options = ts.parseJsonConfigFileContent(
|
|
315
|
+
options = ts.parseJsonConfigFileContent(
|
|
316
|
+
read.config ?? {},
|
|
317
|
+
ts.sys,
|
|
318
|
+
dirname(configPath),
|
|
319
|
+
).options;
|
|
300
320
|
}
|
|
301
321
|
return {
|
|
302
322
|
name: "genroc-ts-resolve",
|
|
303
323
|
resolveId(source, importer) {
|
|
304
324
|
if (!importer || BUILTIN.has(source)) return null;
|
|
305
|
-
const { resolvedModule } = ts.resolveModuleName(
|
|
306
|
-
|
|
307
|
-
|
|
325
|
+
const { resolvedModule } = ts.resolveModuleName(
|
|
326
|
+
source,
|
|
327
|
+
importer,
|
|
328
|
+
options,
|
|
329
|
+
ts.sys,
|
|
330
|
+
);
|
|
331
|
+
if (!resolvedModule || resolvedModule.isExternalLibraryImport)
|
|
332
|
+
return null;
|
|
333
|
+
return resolvedModule.resolvedFileName.endsWith(".d.ts")
|
|
334
|
+
? null
|
|
335
|
+
: resolvedModule.resolvedFileName;
|
|
308
336
|
},
|
|
309
337
|
};
|
|
310
338
|
}
|
|
311
339
|
|
|
312
|
-
/** Bundles to
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
async function bundle(
|
|
316
|
-
|
|
317
|
-
//
|
|
340
|
+
/** Bundles to a self-contained ES module, which is what the evaluator imports: the default
|
|
341
|
+
* export it calls is the author's own, so nothing wraps or rewrites the code between the two.
|
|
342
|
+
* Bundling is entirely the importer's job, so a definition version pins its code forever. */
|
|
343
|
+
async function bundle(at: Located): Promise<string> {
|
|
344
|
+
const site = at.site;
|
|
345
|
+
// Builtins are EXTERNALISED as imports the realm resolves natively. Anything else
|
|
346
|
+
// unresolved is a REFUSAL, not an external: rollup's default is to leave it as an import
|
|
318
347
|
// of a module that will not be there, which bundles clean and fails at runtime.
|
|
319
348
|
const built = await rollup({
|
|
320
|
-
input:
|
|
349
|
+
input: at.file,
|
|
321
350
|
external: (id) => BUILTIN.has(id),
|
|
322
351
|
plugins: [
|
|
323
|
-
tsResolve(await nearestTsconfig(dirname(
|
|
352
|
+
tsResolve(await nearestTsconfig(dirname(at.file), root)),
|
|
324
353
|
nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
|
|
325
354
|
commonjs(),
|
|
326
355
|
// A `.json` import is a data file inlined at build time, which the previous bundler
|
|
@@ -330,23 +359,26 @@ async function bundle(site: Site, root: string): Promise<string> {
|
|
|
330
359
|
],
|
|
331
360
|
onwarn(warning) {
|
|
332
361
|
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
333
|
-
die(
|
|
362
|
+
die(
|
|
363
|
+
`${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`,
|
|
364
|
+
);
|
|
334
365
|
}
|
|
335
366
|
},
|
|
336
|
-
}).catch((e: unknown) =>
|
|
367
|
+
}).catch((e: unknown) =>
|
|
368
|
+
die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`),
|
|
369
|
+
);
|
|
337
370
|
|
|
338
|
-
const { output } = await built.generate({
|
|
371
|
+
const { output } = await built.generate({
|
|
372
|
+
format: "es",
|
|
373
|
+
inlineDynamicImports: true,
|
|
374
|
+
});
|
|
339
375
|
await built.close();
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
` throw new Error(${JSON.stringify(`${site.path} has no default export function`)});`,
|
|
347
|
-
"}",
|
|
348
|
-
"return await __genroc_main(input);",
|
|
349
|
-
].join("\n");
|
|
376
|
+
// Refused here rather than in the realm: the evaluator can only report it against a running
|
|
377
|
+
// instance, and the file it names is on this machine.
|
|
378
|
+
if (!output[0].exports.includes("default")) {
|
|
379
|
+
die(`${at.file}: a script must \`export default\` the function to run`);
|
|
380
|
+
}
|
|
381
|
+
return output[0].code;
|
|
350
382
|
}
|
|
351
383
|
|
|
352
384
|
// ── main ───────────────────────────────────────────────────────────────────────
|
|
@@ -359,40 +391,99 @@ const stdin: string = await new Promise((resolve, reject) => {
|
|
|
359
391
|
process.stdin.on("error", reject);
|
|
360
392
|
});
|
|
361
393
|
const manifest = JSON.parse(stdin) as Manifest;
|
|
362
|
-
|
|
394
|
+
// genctl runs a resolver with the project root as its cwd, so the manifest need not say so.
|
|
395
|
+
const root = process.cwd();
|
|
396
|
+
if (!manifest || !Array.isArray(manifest.processes))
|
|
397
|
+
die("stdin is not a genroc resolver manifest");
|
|
398
|
+
|
|
399
|
+
/** Every site with the process it sits in and the file its argument names, in the manifest's own
|
|
400
|
+
* order — which is the order `code` must answer in. genctl passes the argument verbatim, so
|
|
401
|
+
* joining it to the definition's directory is this resolver's business: only it knows the
|
|
402
|
+
* argument is a path at all. */
|
|
403
|
+
type Located = { site: Site; where: ManifestProcess; file: string };
|
|
404
|
+
|
|
405
|
+
/** A pointer as the address it is, so a generated comment or an error can be pasted into
|
|
406
|
+
* `genctl schema`. A key no identifier can spell is bracketed, as the grammar spells it. */
|
|
407
|
+
function address(pointer: (string | number)[]): string {
|
|
408
|
+
return pointer
|
|
409
|
+
.map((seg) =>
|
|
410
|
+
typeof seg === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)
|
|
411
|
+
? `.${seg}`
|
|
412
|
+
: `[${JSON.stringify(seg)}]`,
|
|
413
|
+
)
|
|
414
|
+
.join("")
|
|
415
|
+
.replace(/^\./, "");
|
|
416
|
+
}
|
|
417
|
+
const located: Located[] = manifest.processes.flatMap((where) =>
|
|
418
|
+
where.sites.map((site) => ({
|
|
419
|
+
site,
|
|
420
|
+
where,
|
|
421
|
+
file: resolve(where.dir, site.argument),
|
|
422
|
+
})),
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
// genctl is agnostic about what a script is for; the contract that an evaluation request carries
|
|
426
|
+
// its module in `code` is THIS resolver's, so it is the one that checks a directive landed there.
|
|
427
|
+
// `child` is the shape the scaffold uses — a call to a process that forwards to the evaluator —
|
|
428
|
+
// and `external` is the same request made directly.
|
|
429
|
+
for (const at of located) {
|
|
430
|
+
// Two halves, from the two places that carry them: the action's KIND is a field, and WHERE in
|
|
431
|
+
// it the directive sits is the pointer. A child call is the shape the scaffold generates — to a
|
|
432
|
+
// process that forwards to the evaluator — and an external task is the same request made
|
|
433
|
+
// directly.
|
|
434
|
+
const kind = at.site.level === "action" ? (at.site.action ?? "") : "";
|
|
435
|
+
const slot = at.site.pointer.slice(-2).join(".");
|
|
436
|
+
if ((kind !== "child" && kind !== "external") || slot !== "input.code") {
|
|
437
|
+
die(
|
|
438
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: an evaluated script ` +
|
|
439
|
+
"belongs in the `code` field of a child or external task's input, and this is " +
|
|
440
|
+
`${kind ? `a ${kind} task's ` : ""}\`${slot}\`.`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// genctl no longer stats the argument — it does not know it is a file — so the resolver that
|
|
446
|
+
// does is the one that must say when it is not there.
|
|
447
|
+
for (const at of located) {
|
|
448
|
+
if (!existsSync(at.file)) {
|
|
449
|
+
die(
|
|
450
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: ` +
|
|
451
|
+
`"$import: ${at.site.argument}" names no file (looked at ${at.file})`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
363
455
|
|
|
364
456
|
// One script at two sites with different input types is a refusal, not a union: the union
|
|
365
457
|
// is sound and would typecheck a body that is wrong at one of the sites.
|
|
366
|
-
const byPath = new Map<string,
|
|
367
|
-
for (const
|
|
368
|
-
const seen = byPath.get(
|
|
369
|
-
const defsOf = (x: Site) => (manifest.schemas[x.process]?.$defs ?? {}) as Record<string, Schema>;
|
|
458
|
+
const byPath = new Map<string, Located>();
|
|
459
|
+
for (const at of located) {
|
|
460
|
+
const seen = byPath.get(at.file);
|
|
370
461
|
if (
|
|
371
462
|
seen &&
|
|
372
|
-
JSON.stringify(
|
|
373
|
-
JSON.stringify(scriptInput(site, defsOf(site)))
|
|
463
|
+
JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)
|
|
374
464
|
) {
|
|
375
465
|
die(
|
|
376
|
-
`${
|
|
466
|
+
`${at.file} is imported at ${address(seen.site.pointer)} and ` +
|
|
467
|
+
`${address(at.site.pointer)} with different input types.\n` +
|
|
377
468
|
"Split it into two scripts, or make the two call sites pass the same shape.",
|
|
378
469
|
);
|
|
379
470
|
}
|
|
380
|
-
byPath.set(
|
|
471
|
+
byPath.set(at.file, at);
|
|
381
472
|
}
|
|
382
473
|
|
|
383
|
-
for (const
|
|
384
|
-
|
|
385
|
-
await write(typesPathFor(site.path), declarations(site, defs));
|
|
474
|
+
for (const at of byPath.values()) {
|
|
475
|
+
await writeFile(typesPathFor(at.file), declarations(at));
|
|
386
476
|
}
|
|
387
477
|
|
|
388
478
|
if (manifest.mode === "types") {
|
|
479
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
389
480
|
process.exit(0);
|
|
390
481
|
}
|
|
391
482
|
|
|
392
|
-
await typecheck(
|
|
483
|
+
await typecheck([...byPath.values()]);
|
|
393
484
|
|
|
394
485
|
const code: string[] = [];
|
|
395
|
-
for (const
|
|
396
|
-
code.push(await bundle(
|
|
486
|
+
for (const at of located) {
|
|
487
|
+
code.push(await bundle(at));
|
|
397
488
|
}
|
|
398
489
|
process.stdout.write(JSON.stringify({ code }));
|