@genroc/eval-node 0.0.0-edge.121ed09 → 0.0.0-edge.24a07d0
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/dist/import.js +77 -41
- package/import.ts +119 -59
- package/package.json +1 -1
package/dist/import.js
CHANGED
|
@@ -7,9 +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
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
10
11
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
11
12
|
import { builtinModules } from "node:module";
|
|
12
|
-
import { dirname, join, relative } from "node:path";
|
|
13
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import commonjs from "@rollup/plugin-commonjs";
|
|
15
16
|
import json from "@rollup/plugin-json";
|
|
@@ -112,21 +113,14 @@ function deref(s, defs) {
|
|
|
112
113
|
}
|
|
113
114
|
return cur;
|
|
114
115
|
}
|
|
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
116
|
/** Emits one named type per reachable $def rather than inlining: a task output may
|
|
125
117
|
* reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
|
|
126
|
-
function declarations(
|
|
118
|
+
function declarations(at) {
|
|
119
|
+
const { site, where } = at;
|
|
120
|
+
const defs = where.$defs ?? {};
|
|
127
121
|
const used = new Set();
|
|
128
|
-
const input = tsType(
|
|
129
|
-
const output = tsType(site.
|
|
122
|
+
const input = tsType(site.types?.Input, used);
|
|
123
|
+
const output = tsType(site.types?.Output, used);
|
|
130
124
|
const emitted = [];
|
|
131
125
|
const done = new Set();
|
|
132
126
|
while (true) {
|
|
@@ -139,7 +133,7 @@ function declarations(site, defs) {
|
|
|
139
133
|
}
|
|
140
134
|
return [
|
|
141
135
|
"// Generated by genroc. Do not edit - regenerate with `genctl types`.",
|
|
142
|
-
`// ${
|
|
136
|
+
`// ${where.name} (${address(site.pointer)})`,
|
|
143
137
|
"",
|
|
144
138
|
...emitted,
|
|
145
139
|
emitted.length ? "" : "",
|
|
@@ -167,7 +161,7 @@ async function nearestTsconfig(from, root) {
|
|
|
167
161
|
return null;
|
|
168
162
|
}
|
|
169
163
|
}
|
|
170
|
-
async function typecheck(
|
|
164
|
+
async function typecheck(sites) {
|
|
171
165
|
// NOT `.genroc`: that is the project config FILE, and a directory of the same name cannot
|
|
172
166
|
// coexist with it. The suffix is what keeps the scratch area out of its way.
|
|
173
167
|
const dir = join(root, ".genroc-cache");
|
|
@@ -175,13 +169,13 @@ async function typecheck(root, sites) {
|
|
|
175
169
|
// One tsc per distinct base config: `extends` takes a single base, so merging two would
|
|
176
170
|
// check each script under the other author's options.
|
|
177
171
|
const groups = new Map();
|
|
178
|
-
for (const
|
|
179
|
-
const base = (await nearestTsconfig(dirname(
|
|
172
|
+
for (const at of sites) {
|
|
173
|
+
const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
|
|
180
174
|
const group = groups.get(base);
|
|
181
175
|
if (group)
|
|
182
|
-
group.push(
|
|
176
|
+
group.push(at);
|
|
183
177
|
else
|
|
184
|
-
groups.set(base, [
|
|
178
|
+
groups.set(base, [at]);
|
|
185
179
|
}
|
|
186
180
|
let n = 0;
|
|
187
181
|
for (const [base, group] of groups) {
|
|
@@ -203,8 +197,8 @@ async function typecheck(root, sites) {
|
|
|
203
197
|
...(base ? {} : { types: [] }),
|
|
204
198
|
},
|
|
205
199
|
files: group.flatMap((s) => [
|
|
206
|
-
relative(dir, s.
|
|
207
|
-
relative(dir, typesPathFor(s.
|
|
200
|
+
relative(dir, s.file),
|
|
201
|
+
relative(dir, typesPathFor(s.file)),
|
|
208
202
|
]),
|
|
209
203
|
// `files` overrides the base's, but a base `include` survives beside it and would
|
|
210
204
|
// drag the author's whole tree in, to be checked under the worker lib.
|
|
@@ -287,15 +281,16 @@ function tsResolve(configPath) {
|
|
|
287
281
|
/** Bundles to a self-contained ES module, which is what the evaluator imports: the default
|
|
288
282
|
* export it calls is the author's own, so nothing wraps or rewrites the code between the two.
|
|
289
283
|
* Bundling is entirely the importer's job, so a definition version pins its code forever. */
|
|
290
|
-
async function bundle(
|
|
284
|
+
async function bundle(at) {
|
|
285
|
+
const site = at.site;
|
|
291
286
|
// Builtins are EXTERNALISED as imports the realm resolves natively. Anything else
|
|
292
287
|
// unresolved is a REFUSAL, not an external: rollup's default is to leave it as an import
|
|
293
288
|
// of a module that will not be there, which bundles clean and fails at runtime.
|
|
294
289
|
const built = await rollup({
|
|
295
|
-
input:
|
|
290
|
+
input: at.file,
|
|
296
291
|
external: (id) => BUILTIN.has(id),
|
|
297
292
|
plugins: [
|
|
298
|
-
tsResolve(await nearestTsconfig(dirname(
|
|
293
|
+
tsResolve(await nearestTsconfig(dirname(at.file), root)),
|
|
299
294
|
nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
|
|
300
295
|
commonjs(),
|
|
301
296
|
// A `.json` import is a data file inlined at build time, which the previous bundler
|
|
@@ -305,10 +300,10 @@ async function bundle(site, root) {
|
|
|
305
300
|
],
|
|
306
301
|
onwarn(warning) {
|
|
307
302
|
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
308
|
-
die(`${
|
|
303
|
+
die(`${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`);
|
|
309
304
|
}
|
|
310
305
|
},
|
|
311
|
-
}).catch((e) => die(`${
|
|
306
|
+
}).catch((e) => die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`));
|
|
312
307
|
const { output } = await built.generate({
|
|
313
308
|
format: "es",
|
|
314
309
|
inlineDynamicImports: true,
|
|
@@ -317,7 +312,7 @@ async function bundle(site, root) {
|
|
|
317
312
|
// Refused here rather than in the realm: the evaluator can only report it against a running
|
|
318
313
|
// instance, and the file it names is on this machine.
|
|
319
314
|
if (!output[0].exports.includes("default")) {
|
|
320
|
-
die(`${
|
|
315
|
+
die(`${at.file}: a script must \`export default\` the function to run`);
|
|
321
316
|
}
|
|
322
317
|
return output[0].code;
|
|
323
318
|
}
|
|
@@ -330,32 +325,73 @@ const stdin = await new Promise((resolve, reject) => {
|
|
|
330
325
|
process.stdin.on("error", reject);
|
|
331
326
|
});
|
|
332
327
|
const manifest = JSON.parse(stdin);
|
|
333
|
-
|
|
328
|
+
// genctl runs a resolver with the project root as its cwd, so the manifest need not say so.
|
|
329
|
+
const root = process.cwd();
|
|
330
|
+
if (!manifest || !Array.isArray(manifest.processes))
|
|
334
331
|
die("stdin is not a genroc resolver manifest");
|
|
332
|
+
/** A pointer as the address it is, so a generated comment or an error can be pasted into
|
|
333
|
+
* `genctl schema`. A key no identifier can spell is bracketed, as the grammar spells it. */
|
|
334
|
+
function address(pointer) {
|
|
335
|
+
return pointer
|
|
336
|
+
.map((seg) => typeof seg === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)
|
|
337
|
+
? `.${seg}`
|
|
338
|
+
: `[${JSON.stringify(seg)}]`)
|
|
339
|
+
.join("")
|
|
340
|
+
.replace(/^\./, "");
|
|
341
|
+
}
|
|
342
|
+
const located = manifest.processes.flatMap((where) => where.sites.map((site) => ({
|
|
343
|
+
site,
|
|
344
|
+
where,
|
|
345
|
+
file: resolve(where.dir, site.argument),
|
|
346
|
+
})));
|
|
347
|
+
// genctl is agnostic about what a script is for; the contract that an evaluation request carries
|
|
348
|
+
// its module in `code` is THIS resolver's, so it is the one that checks a directive landed there.
|
|
349
|
+
// `child` is the shape the scaffold uses — a call to a process that forwards to the evaluator —
|
|
350
|
+
// and `external` is the same request made directly.
|
|
351
|
+
for (const at of located) {
|
|
352
|
+
// Two halves, from the two places that carry them: the action's KIND is a field, and WHERE in
|
|
353
|
+
// it the directive sits is the pointer. A child call is the shape the scaffold generates — to a
|
|
354
|
+
// process that forwards to the evaluator — and an external task is the same request made
|
|
355
|
+
// directly.
|
|
356
|
+
const kind = at.site.action ?? "";
|
|
357
|
+
const slot = at.site.pointer.slice(-2).join(".");
|
|
358
|
+
if ((kind !== "child" && kind !== "external") || slot !== "input.code") {
|
|
359
|
+
die(`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: an evaluated script ` +
|
|
360
|
+
"belongs in the `code` field of a child or external task's input, and this is " +
|
|
361
|
+
`${kind ? `a ${kind} task's ` : ""}\`${slot}\`.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// genctl no longer stats the argument — it does not know it is a file — so the resolver that
|
|
365
|
+
// does is the one that must say when it is not there.
|
|
366
|
+
for (const at of located) {
|
|
367
|
+
if (!existsSync(at.file)) {
|
|
368
|
+
die(`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: ` +
|
|
369
|
+
`"$import: ${at.site.argument}" names no file (looked at ${at.file})`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
335
372
|
// One script at two sites with different input types is a refusal, not a union: the union
|
|
336
373
|
// is sound and would typecheck a body that is wrong at one of the sites.
|
|
337
374
|
const byPath = new Map();
|
|
338
|
-
for (const
|
|
339
|
-
const seen = byPath.get(
|
|
340
|
-
const defsOf = (x) => (manifest.schemas[x.process]?.$defs ?? {});
|
|
375
|
+
for (const at of located) {
|
|
376
|
+
const seen = byPath.get(at.file);
|
|
341
377
|
if (seen &&
|
|
342
|
-
JSON.stringify(
|
|
343
|
-
|
|
344
|
-
|
|
378
|
+
JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)) {
|
|
379
|
+
die(`${at.file} is imported at ${address(seen.site.pointer)} and ` +
|
|
380
|
+
`${address(at.site.pointer)} with different input types.\n` +
|
|
345
381
|
"Split it into two scripts, or make the two call sites pass the same shape.");
|
|
346
382
|
}
|
|
347
|
-
byPath.set(
|
|
383
|
+
byPath.set(at.file, at);
|
|
348
384
|
}
|
|
349
|
-
for (const
|
|
350
|
-
|
|
351
|
-
await write(typesPathFor(site.path), declarations(site, defs));
|
|
385
|
+
for (const at of byPath.values()) {
|
|
386
|
+
await write(typesPathFor(at.file), declarations(at));
|
|
352
387
|
}
|
|
353
388
|
if (manifest.mode === "types") {
|
|
389
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
354
390
|
process.exit(0);
|
|
355
391
|
}
|
|
356
|
-
await typecheck(
|
|
392
|
+
await typecheck([...byPath.values()]);
|
|
357
393
|
const code = [];
|
|
358
|
-
for (const
|
|
359
|
-
code.push(await bundle(
|
|
394
|
+
for (const at of located) {
|
|
395
|
+
code.push(await bundle(at));
|
|
360
396
|
}
|
|
361
397
|
process.stdout.write(JSON.stringify({ code }));
|
package/import.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// over the same project.
|
|
9
9
|
|
|
10
10
|
import { spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
11
12
|
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
12
13
|
import { builtinModules } from "node:module";
|
|
13
|
-
import { dirname, join, relative } from "node:path";
|
|
14
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
|
|
16
17
|
import commonjs from "@rollup/plugin-commonjs";
|
|
@@ -22,20 +23,35 @@ import ts from "typescript";
|
|
|
22
23
|
type Schema = Record<string, any>;
|
|
23
24
|
|
|
24
25
|
type Site = {
|
|
25
|
-
resolver: string;
|
|
26
|
-
process: string;
|
|
27
26
|
task?: string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// What the site IS: the task's action type, and the process a child action calls.
|
|
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 {
|
|
@@ -150,25 +166,14 @@ function deref(
|
|
|
150
166
|
return cur;
|
|
151
167
|
}
|
|
152
168
|
|
|
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
169
|
/** Emits one named type per reachable $def rather than inlining: a task output may
|
|
167
170
|
* reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
|
|
168
|
-
function declarations(
|
|
171
|
+
function declarations(at: Located): string {
|
|
172
|
+
const { site, where } = at;
|
|
173
|
+
const defs = where.$defs ?? {};
|
|
169
174
|
const used = new Set<string>();
|
|
170
|
-
const input = tsType(
|
|
171
|
-
const output = tsType(site.
|
|
175
|
+
const input = tsType(site.types?.Input, used);
|
|
176
|
+
const output = tsType(site.types?.Output, used);
|
|
172
177
|
|
|
173
178
|
const emitted: string[] = [];
|
|
174
179
|
const done = new Set<string>();
|
|
@@ -182,7 +187,7 @@ function declarations(site: Site, defs: Record<string, Schema>): string {
|
|
|
182
187
|
|
|
183
188
|
return [
|
|
184
189
|
"// Generated by genroc. Do not edit - regenerate with `genctl types`.",
|
|
185
|
-
`// ${
|
|
190
|
+
`// ${where.name} (${address(site.pointer)})`,
|
|
186
191
|
"",
|
|
187
192
|
...emitted,
|
|
188
193
|
emitted.length ? "" : "",
|
|
@@ -215,7 +220,7 @@ async function nearestTsconfig(
|
|
|
215
220
|
}
|
|
216
221
|
}
|
|
217
222
|
|
|
218
|
-
async function typecheck(
|
|
223
|
+
async function typecheck(sites: Located[]): Promise<void> {
|
|
219
224
|
// NOT `.genroc`: that is the project config FILE, and a directory of the same name cannot
|
|
220
225
|
// coexist with it. The suffix is what keeps the scratch area out of its way.
|
|
221
226
|
const dir = join(root, ".genroc-cache");
|
|
@@ -223,12 +228,12 @@ async function typecheck(root: string, sites: Site[]): Promise<void> {
|
|
|
223
228
|
|
|
224
229
|
// One tsc per distinct base config: `extends` takes a single base, so merging two would
|
|
225
230
|
// check each script under the other author's options.
|
|
226
|
-
const groups = new Map<string,
|
|
227
|
-
for (const
|
|
228
|
-
const base = (await nearestTsconfig(dirname(
|
|
231
|
+
const groups = new Map<string, Located[]>();
|
|
232
|
+
for (const at of sites) {
|
|
233
|
+
const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
|
|
229
234
|
const group = groups.get(base);
|
|
230
|
-
if (group) group.push(
|
|
231
|
-
else groups.set(base, [
|
|
235
|
+
if (group) group.push(at);
|
|
236
|
+
else groups.set(base, [at]);
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
let n = 0;
|
|
@@ -251,8 +256,8 @@ async function typecheck(root: string, sites: Site[]): Promise<void> {
|
|
|
251
256
|
...(base ? {} : { types: [] }),
|
|
252
257
|
},
|
|
253
258
|
files: group.flatMap((s) => [
|
|
254
|
-
relative(dir, s.
|
|
255
|
-
relative(dir, typesPathFor(s.
|
|
259
|
+
relative(dir, s.file),
|
|
260
|
+
relative(dir, typesPathFor(s.file)),
|
|
256
261
|
]),
|
|
257
262
|
// `files` overrides the base's, but a base `include` survives beside it and would
|
|
258
263
|
// drag the author's whole tree in, to be checked under the worker lib.
|
|
@@ -351,15 +356,16 @@ function tsResolve(configPath: string | null): Plugin {
|
|
|
351
356
|
/** Bundles to a self-contained ES module, which is what the evaluator imports: the default
|
|
352
357
|
* export it calls is the author's own, so nothing wraps or rewrites the code between the two.
|
|
353
358
|
* Bundling is entirely the importer's job, so a definition version pins its code forever. */
|
|
354
|
-
async function bundle(
|
|
359
|
+
async function bundle(at: Located): Promise<string> {
|
|
360
|
+
const site = at.site;
|
|
355
361
|
// Builtins are EXTERNALISED as imports the realm resolves natively. Anything else
|
|
356
362
|
// unresolved is a REFUSAL, not an external: rollup's default is to leave it as an import
|
|
357
363
|
// of a module that will not be there, which bundles clean and fails at runtime.
|
|
358
364
|
const built = await rollup({
|
|
359
|
-
input:
|
|
365
|
+
input: at.file,
|
|
360
366
|
external: (id) => BUILTIN.has(id),
|
|
361
367
|
plugins: [
|
|
362
|
-
tsResolve(await nearestTsconfig(dirname(
|
|
368
|
+
tsResolve(await nearestTsconfig(dirname(at.file), root)),
|
|
363
369
|
nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
|
|
364
370
|
commonjs(),
|
|
365
371
|
// A `.json` import is a data file inlined at build time, which the previous bundler
|
|
@@ -370,12 +376,12 @@ async function bundle(site: Site, root: string): Promise<string> {
|
|
|
370
376
|
onwarn(warning) {
|
|
371
377
|
if (warning.code === "UNRESOLVED_IMPORT") {
|
|
372
378
|
die(
|
|
373
|
-
`${
|
|
379
|
+
`${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`,
|
|
374
380
|
);
|
|
375
381
|
}
|
|
376
382
|
},
|
|
377
383
|
}).catch((e: unknown) =>
|
|
378
|
-
die(`${
|
|
384
|
+
die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`),
|
|
379
385
|
);
|
|
380
386
|
|
|
381
387
|
const { output } = await built.generate({
|
|
@@ -386,7 +392,7 @@ async function bundle(site: Site, root: string): Promise<string> {
|
|
|
386
392
|
// Refused here rather than in the realm: the evaluator can only report it against a running
|
|
387
393
|
// instance, and the file it names is on this machine.
|
|
388
394
|
if (!output[0].exports.includes("default")) {
|
|
389
|
-
die(`${
|
|
395
|
+
die(`${at.file}: a script must \`export default\` the function to run`);
|
|
390
396
|
}
|
|
391
397
|
return output[0].code;
|
|
392
398
|
}
|
|
@@ -401,45 +407,99 @@ const stdin: string = await new Promise((resolve, reject) => {
|
|
|
401
407
|
process.stdin.on("error", reject);
|
|
402
408
|
});
|
|
403
409
|
const manifest = JSON.parse(stdin) as Manifest;
|
|
404
|
-
|
|
410
|
+
// genctl runs a resolver with the project root as its cwd, so the manifest need not say so.
|
|
411
|
+
const root = process.cwd();
|
|
412
|
+
if (!manifest || !Array.isArray(manifest.processes))
|
|
405
413
|
die("stdin is not a genroc resolver manifest");
|
|
406
414
|
|
|
415
|
+
/** Every site with the process it sits in and the file its argument names, in the manifest's own
|
|
416
|
+
* order — which is the order `code` must answer in. genctl passes the argument verbatim, so
|
|
417
|
+
* joining it to the definition's directory is this resolver's business: only it knows the
|
|
418
|
+
* argument is a path at all. */
|
|
419
|
+
type Located = { site: Site; where: ManifestProcess; file: string };
|
|
420
|
+
|
|
421
|
+
/** A pointer as the address it is, so a generated comment or an error can be pasted into
|
|
422
|
+
* `genctl schema`. A key no identifier can spell is bracketed, as the grammar spells it. */
|
|
423
|
+
function address(pointer: (string | number)[]): string {
|
|
424
|
+
return pointer
|
|
425
|
+
.map((seg) =>
|
|
426
|
+
typeof seg === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(seg)
|
|
427
|
+
? `.${seg}`
|
|
428
|
+
: `[${JSON.stringify(seg)}]`,
|
|
429
|
+
)
|
|
430
|
+
.join("")
|
|
431
|
+
.replace(/^\./, "");
|
|
432
|
+
}
|
|
433
|
+
const located: Located[] = manifest.processes.flatMap((where) =>
|
|
434
|
+
where.sites.map((site) => ({
|
|
435
|
+
site,
|
|
436
|
+
where,
|
|
437
|
+
file: resolve(where.dir, site.argument),
|
|
438
|
+
})),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// genctl is agnostic about what a script is for; the contract that an evaluation request carries
|
|
442
|
+
// its module in `code` is THIS resolver's, so it is the one that checks a directive landed there.
|
|
443
|
+
// `child` is the shape the scaffold uses — a call to a process that forwards to the evaluator —
|
|
444
|
+
// and `external` is the same request made directly.
|
|
445
|
+
for (const at of located) {
|
|
446
|
+
// Two halves, from the two places that carry them: the action's KIND is a field, and WHERE in
|
|
447
|
+
// it the directive sits is the pointer. A child call is the shape the scaffold generates — to a
|
|
448
|
+
// process that forwards to the evaluator — and an external task is the same request made
|
|
449
|
+
// directly.
|
|
450
|
+
const kind = at.site.action ?? "";
|
|
451
|
+
const slot = at.site.pointer.slice(-2).join(".");
|
|
452
|
+
if ((kind !== "child" && kind !== "external") || slot !== "input.code") {
|
|
453
|
+
die(
|
|
454
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: an evaluated script ` +
|
|
455
|
+
"belongs in the `code` field of a child or external task's input, and this is " +
|
|
456
|
+
`${kind ? `a ${kind} task's ` : ""}\`${slot}\`.`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// genctl no longer stats the argument — it does not know it is a file — so the resolver that
|
|
462
|
+
// does is the one that must say when it is not there.
|
|
463
|
+
for (const at of located) {
|
|
464
|
+
if (!existsSync(at.file)) {
|
|
465
|
+
die(
|
|
466
|
+
`${join(at.where.dir, at.where.file)}: ${address(at.site.pointer)}: ` +
|
|
467
|
+
`"$import: ${at.site.argument}" names no file (looked at ${at.file})`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
407
472
|
// One script at two sites with different input types is a refusal, not a union: the union
|
|
408
473
|
// 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>;
|
|
474
|
+
const byPath = new Map<string, Located>();
|
|
475
|
+
for (const at of located) {
|
|
476
|
+
const seen = byPath.get(at.file);
|
|
414
477
|
if (
|
|
415
478
|
seen &&
|
|
416
|
-
JSON.stringify(
|
|
417
|
-
JSON.stringify(scriptInput(site, defsOf(site)))
|
|
479
|
+
JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)
|
|
418
480
|
) {
|
|
419
481
|
die(
|
|
420
|
-
`${
|
|
482
|
+
`${at.file} is imported at ${address(seen.site.pointer)} and ` +
|
|
483
|
+
`${address(at.site.pointer)} with different input types.\n` +
|
|
421
484
|
"Split it into two scripts, or make the two call sites pass the same shape.",
|
|
422
485
|
);
|
|
423
486
|
}
|
|
424
|
-
byPath.set(
|
|
487
|
+
byPath.set(at.file, at);
|
|
425
488
|
}
|
|
426
489
|
|
|
427
|
-
for (const
|
|
428
|
-
|
|
429
|
-
string,
|
|
430
|
-
Schema
|
|
431
|
-
>;
|
|
432
|
-
await write(typesPathFor(site.path), declarations(site, defs));
|
|
490
|
+
for (const at of byPath.values()) {
|
|
491
|
+
await write(typesPathFor(at.file), declarations(at));
|
|
433
492
|
}
|
|
434
493
|
|
|
435
494
|
if (manifest.mode === "types") {
|
|
495
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
436
496
|
process.exit(0);
|
|
437
497
|
}
|
|
438
498
|
|
|
439
|
-
await typecheck(
|
|
499
|
+
await typecheck([...byPath.values()]);
|
|
440
500
|
|
|
441
501
|
const code: string[] = [];
|
|
442
|
-
for (const
|
|
443
|
-
code.push(await bundle(
|
|
502
|
+
for (const at of located) {
|
|
503
|
+
code.push(await bundle(at));
|
|
444
504
|
}
|
|
445
505
|
process.stdout.write(JSON.stringify({ code }));
|