@genroc/eval-node 0.0.0-edge.4348678 → 0.0.0-edge.4394f2ae

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/import.ts CHANGED
@@ -1,17 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // The code-phase resolver: manifest on stdin, `{"code": [...]}` on stdout, non-zero exit
3
- // with the diagnostic on stderr. genctl never parses TypeScript and this never parses YAML
4
- // — the manifest is the whole contract. See specs/source-resolution.md.
5
- //
6
- // Two modes, one binary: "types" writes the declarations an editor needs and returns no
7
- // code; "build" typechecks and bundles. A separate types hook would mean a second `tsc`
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, relative } from "node:path";
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
- resolver: string;
26
- process: string;
20
+ // Which namespace the directive sits in: `process`, `task` or `action`.
21
+ level: "process" | "task" | "action";
27
22
  task?: string;
28
- pointer: string;
29
- path: string;
30
- input?: Schema;
31
- output?: Schema;
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 Manifest = {
35
- mode: "types" | "build";
36
- root: string;
37
- schemas: Record<string, Schema>;
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
@@ -72,13 +77,16 @@ function tsType(s: Schema | undefined, used: Set<string>): string {
72
77
  if (Array.isArray(s.enum)) {
73
78
  return s.enum.map((v: unknown) => JSON.stringify(v)).join(" | ") || "never";
74
79
  }
75
- if (Array.isArray(s.anyOf)) return union(s.anyOf.map((a: Schema) => tsType(a, used)));
76
- if (Array.isArray(s.oneOf)) return union(s.oneOf.map((a: Schema) => tsType(a, used)));
80
+ if (Array.isArray(s.anyOf))
81
+ return union(s.anyOf.map((a: Schema) => tsType(a, used)));
82
+ if (Array.isArray(s.oneOf))
83
+ return union(s.oneOf.map((a: Schema) => tsType(a, used)));
77
84
  if (Array.isArray(s.allOf)) {
78
85
  return s.allOf.map((a: Schema) => tsType(a, used)).join(" & ") || "unknown";
79
86
  }
80
87
 
81
- const types: string[] = s.type === undefined ? [] : Array.isArray(s.type) ? s.type : [s.type];
88
+ const types: string[] =
89
+ s.type === undefined ? [] : Array.isArray(s.type) ? s.type : [s.type];
82
90
  if (types.length === 0) {
83
91
  // The top type: `{}` means unknown, not "an empty object". specs/unknown-type.md.
84
92
  return s.properties ? objectType(s, used) : "unknown";
@@ -111,16 +119,26 @@ function objectType(s: Schema, used: Set<string>): string {
111
119
  const required = new Set<string>(s.required ?? []);
112
120
  const lines: string[] = [];
113
121
  for (const [key, sub] of Object.entries(props)) {
114
- const doc = typeof sub.description === "string" ? ` /** ${sub.description} */\n` : "";
115
- lines.push(`${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${tsType(sub, used)};`);
122
+ const doc =
123
+ typeof sub.description === "string"
124
+ ? ` /** ${sub.description} */\n`
125
+ : "";
126
+ lines.push(
127
+ `${doc} ${propKey(key)}${required.has(key) ? "" : "?"}: ${nest(tsType(sub, used))};`,
128
+ );
116
129
  }
117
130
  if (s.additionalProperties && typeof s.additionalProperties === "object") {
118
- lines.push(` [key: string]: ${tsType(s.additionalProperties, used)};`);
131
+ lines.push(
132
+ ` [key: string]: ${nest(tsType(s.additionalProperties, used))};`,
133
+ );
119
134
  }
120
135
  if (lines.length === 0) return "Record<string, unknown>";
121
136
  return `{\n${lines.join("\n")}\n}`;
122
137
  }
123
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
+
124
142
  function union(parts: string[]): string {
125
143
  const seen = [...new Set(parts)];
126
144
  return seen.length === 0 ? "unknown" : seen.join(" | ");
@@ -128,32 +146,17 @@ function union(parts: string[]): string {
128
146
 
129
147
  const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
130
148
  const propKey = (k: string) => (IDENT.test(k) ? k : JSON.stringify(k));
131
- const identifier = (n: string) => (IDENT.test(n) ? n : `Def_${n.replace(/[^A-Za-z0-9_$]/g, "_")}`);
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
- }
149
+ const identifier = (n: string) =>
150
+ IDENT.test(n) ? n : `Def_${n.replace(/[^A-Za-z0-9_$]/g, "_")}`;
150
151
 
151
152
  /** Emits one named type per reachable $def rather than inlining: a task output may
152
153
  * reference itself (specs/recursive-type-inference.md) and inlining would not terminate. */
153
- function declarations(site: Site, defs: Record<string, Schema>): string {
154
+ function declarations(at: Located): string {
155
+ const { site, where } = at;
156
+ const defs = where.$defs ?? {};
154
157
  const used = new Set<string>();
155
- const input = tsType(scriptInput(site, defs), used);
156
- const output = tsType(site.output, used);
158
+ const input = tsType(site.types?.Input, used);
159
+ const output = tsType(site.types?.Output, used);
157
160
 
158
161
  const emitted: string[] = [];
159
162
  const done = new Set<string>();
@@ -166,8 +169,8 @@ function declarations(site: Site, defs: Record<string, Schema>): string {
166
169
  }
167
170
 
168
171
  return [
169
- "// Generated by genroc. Do not edit - regenerate with `genctl types`.",
170
- `// ${site.process}${site.task ? ` / ${site.task}` : ""} (${site.pointer})`,
172
+ "// Generated by genroc. Do not edit - regenerate with `genctl generate`.",
173
+ `// ${where.name} (${address(site.pointer)})`,
171
174
  "",
172
175
  ...emitted,
173
176
  emitted.length ? "" : "",
@@ -186,10 +189,12 @@ function typesPathFor(scriptPath: string): string {
186
189
 
187
190
  // ── typecheck ──────────────────────────────────────────────────────────────────
188
191
 
189
- /** The nearest tsconfig above the script — the one the author's editor already reads. Two
190
- * different configs mean a red editor over a clean apply, or the reverse. The walk stops at
191
- * the project root: above it is not this project. */
192
- async function nearestTsconfig(from: string, root: string): Promise<string | null> {
192
+ /** The tsconfig the author's editor reads, so editor and apply agree. Stops at the project
193
+ * root. */
194
+ async function nearestTsconfig(
195
+ from: string,
196
+ root: string,
197
+ ): Promise<string | null> {
193
198
  for (let dir = from; ; dir = dirname(dir)) {
194
199
  const candidate = join(dir, "tsconfig.json");
195
200
  if (await exists(candidate)) return candidate;
@@ -197,26 +202,21 @@ async function nearestTsconfig(from: string, root: string): Promise<string | nul
197
202
  }
198
203
  }
199
204
 
200
- async function typecheck(root: string, sites: Site[]): Promise<void> {
201
- // NOT `.genroc`: that is the project config FILE, and a directory of the same name cannot
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
205
+ async function typecheck(sites: Located[]): Promise<void> {
206
+ // One program per distinct base config: `extends` takes a single base, so merging two would
207
207
  // check each script under the other author's options.
208
- const groups = new Map<string, Site[]>();
209
- for (const site of sites) {
210
- const base = (await nearestTsconfig(dirname(site.path), root)) ?? "";
208
+ const groups = new Map<string, Located[]>();
209
+ for (const at of sites) {
210
+ const base = (await nearestTsconfig(dirname(at.file), root)) ?? "";
211
211
  const group = groups.get(base);
212
- if (group) group.push(site);
213
- else groups.set(base, [site]);
212
+ if (group) group.push(at);
213
+ else groups.set(base, [at]);
214
214
  }
215
215
 
216
- let n = 0;
216
+ const diagnostics: ts.Diagnostic[] = [];
217
217
  for (const [base, group] of groups) {
218
218
  const config: Record<string, unknown> = {
219
- ...(base ? { extends: relative(dir, base) } : {}),
219
+ ...(base ? { extends: base } : {}),
220
220
  compilerOptions: {
221
221
  noEmit: true,
222
222
  strict: true,
@@ -227,40 +227,45 @@ async function typecheck(root: string, sites: Site[]): Promise<void> {
227
227
  // `lib` DESCRIBES the realm and is written after `extends` so a base cannot widen it:
228
228
  // a worker thread has no document, whatever an author's config claims.
229
229
  lib: ["esnext", "webworker"],
230
- // `types` is the author's, and it is how a script opts into the node globals —
231
- // the worker realm has them, so refusing the declarations would only lie. With no
232
- // 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.
233
232
  ...(base ? {} : { types: [] }),
234
233
  },
235
- files: group.flatMap((s) => [relative(dir, s.path), relative(dir, typesPathFor(s.path))]),
234
+ files: group.flatMap((s) => [s.file, typesPathFor(s.file)]),
236
235
  // `files` overrides the base's, but a base `include` survives beside it and would
237
236
  // drag the author's whole tree in, to be checked under the worker lib.
238
237
  include: [],
239
238
  };
240
- const configPath = join(dir, groups.size === 1 ? "tsconfig.json" : `tsconfig.${n++}.json`);
241
- await write(configPath, JSON.stringify(config, null, 2));
242
- await runTsc(root, configPath);
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"),
247
+ );
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));
243
255
  }
244
- }
245
256
 
246
- async function runTsc(root: string, configPath: string): Promise<void> {
247
- const tsc = fileURLToPath(import.meta.resolve("typescript/bin/tsc"));
248
- const proc = spawn(process.execPath, [tsc, "--noEmit", "-p", configPath], {
249
- cwd: root,
250
- stdio: ["ignore", "pipe", "pipe"],
251
- });
252
- let out = "";
253
- let err = "";
254
- proc.stdout.on("data", (c: Buffer) => (out += c));
255
- proc.stderr.on("data", (c: Buffer) => (err += c));
256
- const code = await new Promise<number>((resolve, reject) => {
257
- proc.on("error", reject);
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());
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
+ );
264
269
  }
265
270
  }
266
271
 
@@ -285,68 +290,82 @@ const transpile: Plugin = {
285
290
  },
286
291
  };
287
292
 
288
- const BUILTIN = new Set([...builtinModules, ...builtinModules.map((m) => `node:${m}`)]);
293
+ const BUILTIN = new Set([
294
+ ...builtinModules,
295
+ ...builtinModules.map((m) => `node:${m}`),
296
+ ]);
289
297
 
290
- /** Resolves imports through TYPESCRIPT, using the same config the typecheck ran under, so a
291
- * `paths` alias that compiles also bundles. Reimplementing `paths` here would be a second
292
- * resolver to keep in agreement with tsc; this one cannot disagree.
293
- * A package resolving to a `.d.ts` is declined — that is a type, not the implementation —
294
- * 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. */
295
301
  function tsResolve(configPath: string | null): Plugin {
296
302
  let options: ts.CompilerOptions = {};
297
303
  if (configPath) {
298
304
  const read = ts.readConfigFile(configPath, ts.sys.readFile);
299
- options = ts.parseJsonConfigFileContent(read.config ?? {}, ts.sys, dirname(configPath)).options;
305
+ options = ts.parseJsonConfigFileContent(
306
+ read.config ?? {},
307
+ ts.sys,
308
+ dirname(configPath),
309
+ ).options;
300
310
  }
301
311
  return {
302
312
  name: "genroc-ts-resolve",
303
313
  resolveId(source, importer) {
304
314
  if (!importer || BUILTIN.has(source)) return null;
305
- const { resolvedModule } = ts.resolveModuleName(source, importer, options, ts.sys);
306
- if (!resolvedModule || resolvedModule.isExternalLibraryImport) return null;
307
- return resolvedModule.resolvedFileName.endsWith(".d.ts") ? null : resolvedModule.resolvedFileName;
315
+ const { resolvedModule } = ts.resolveModuleName(
316
+ source,
317
+ importer,
318
+ options,
319
+ ts.sys,
320
+ );
321
+ if (!resolvedModule || resolvedModule.isExternalLibraryImport)
322
+ return null;
323
+ return resolvedModule.resolvedFileName.endsWith(".d.ts")
324
+ ? null
325
+ : resolvedModule.resolvedFileName;
308
326
  },
309
327
  };
310
328
  }
311
329
 
312
- /** Bundles to CJS and wraps it as an async function BODY, which is what /eval compiles.
313
- * The runtime stays unchanged: bundling is entirely the importer's job, and the string it
314
- * produces is self-contained, so a definition version pins its code forever. */
315
- async function bundle(site: Site, root: string): Promise<string> {
316
- // Builtins are EXTERNALISED as `require` calls that worker.ts satisfies. Anything else
317
- // unresolved is a REFUSAL, not an external: rollup's default is to leave it as a require
318
- // 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.
319
336
  const built = await rollup({
320
- input: site.path,
337
+ input: at.file,
321
338
  external: (id) => BUILTIN.has(id),
322
339
  plugins: [
323
- tsResolve(await nearestTsconfig(dirname(site.path), root)),
340
+ tsResolve(await nearestTsconfig(dirname(at.file), root)),
324
341
  nodeResolve({ extensions: [".ts", ".tsx", ".mjs", ".js", ".json"] }),
325
342
  commonjs(),
326
- // A `.json` import is a data file inlined at build time, which the previous bundler
327
- // 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.
328
344
  json(),
329
345
  transpile,
330
346
  ],
331
347
  onwarn(warning) {
332
348
  if (warning.code === "UNRESOLVED_IMPORT") {
333
- die(`${site.path}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`);
349
+ die(
350
+ `${at.file}: cannot resolve ${warning.exporter ?? "an import"} — is it installed?`,
351
+ );
334
352
  }
335
353
  },
336
- }).catch((e: unknown) => die(`${site.path}: ${e instanceof Error ? e.message : String(e)}`));
354
+ }).catch((e: unknown) =>
355
+ die(`${at.file}: ${e instanceof Error ? e.message : String(e)}`),
356
+ );
337
357
 
338
- const { output } = await built.generate({ format: "cjs", exports: "auto", inlineDynamicImports: true });
358
+ const { output } = await built.generate({
359
+ format: "es",
360
+ inlineDynamicImports: true,
361
+ });
339
362
  await built.close();
340
- const cjs = output[0].code;
341
- return [
342
- "var module = { exports: {} }, exports = module.exports;",
343
- cjs,
344
- "var __genroc_main = module.exports.default ?? module.exports;",
345
- 'if (typeof __genroc_main !== "function") {',
346
- ` throw new Error(${JSON.stringify(`${site.path} has no default export function`)});`,
347
- "}",
348
- "return await __genroc_main(input);",
349
- ].join("\n");
363
+ // Refused here rather than in the realm: the evaluator can only report it against a running
364
+ // instance, and the file it names is on this machine.
365
+ if (!output[0].exports.includes("default")) {
366
+ die(`${at.file}: a script must \`export default\` the function to run`);
367
+ }
368
+ return output[0].code;
350
369
  }
351
370
 
352
371
  // ── main ───────────────────────────────────────────────────────────────────────
@@ -359,40 +378,95 @@ const stdin: string = await new Promise((resolve, reject) => {
359
378
  process.stdin.on("error", reject);
360
379
  });
361
380
  const manifest = JSON.parse(stdin) as Manifest;
362
- if (!manifest || !Array.isArray(manifest.sites)) die("stdin is not a genroc resolver manifest");
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))
384
+ die("stdin is not a genroc resolver manifest");
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
+ }
363
439
 
364
440
  // One script at two sites with different input types is a refusal, not a union: the union
365
441
  // is sound and would typecheck a body that is wrong at one of the sites.
366
- const byPath = new Map<string, Site>();
367
- for (const site of manifest.sites) {
368
- const seen = byPath.get(site.path);
369
- const defsOf = (x: Site) => (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);
370
445
  if (
371
446
  seen &&
372
- JSON.stringify(scriptInput(seen, defsOf(seen))) !==
373
- JSON.stringify(scriptInput(site, defsOf(site)))
447
+ JSON.stringify(seen.site.types) !== JSON.stringify(at.site.types)
374
448
  ) {
375
449
  die(
376
- `${site.path} is imported at ${seen.pointer} and ${site.pointer} with different input types.\n` +
450
+ `${at.file} is imported at ${address(seen.site.pointer)} and ` +
451
+ `${address(at.site.pointer)} with different input types.\n` +
377
452
  "Split it into two scripts, or make the two call sites pass the same shape.",
378
453
  );
379
454
  }
380
- byPath.set(site.path, site);
455
+ byPath.set(at.file, at);
381
456
  }
382
457
 
383
- for (const site of byPath.values()) {
384
- const defs = (manifest.schemas[site.process]?.$defs ?? {}) as Record<string, Schema>;
385
- await write(typesPathFor(site.path), declarations(site, defs));
458
+ for (const at of byPath.values()) {
459
+ await writeFile(typesPathFor(at.file), declarations(at));
386
460
  }
387
461
 
388
- if (manifest.mode === "types") {
462
+ if (manifest.mode === "generate") {
389
463
  process.exit(0);
390
464
  }
391
465
 
392
- await typecheck(manifest.root, [...byPath.values()]);
466
+ await typecheck([...byPath.values()]);
393
467
 
394
- const code: string[] = [];
395
- for (const site of manifest.sites) {
396
- code.push(await bundle(site, manifest.root));
468
+ const values: string[] = [];
469
+ for (const at of located) {
470
+ values.push(await bundle(at));
397
471
  }
398
- process.stdout.write(JSON.stringify({ code }));
472
+ process.stdout.write(JSON.stringify({ values }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genroc/eval-node",
3
- "version": "0.0.0-edge.4348678",
3
+ "version": "0.0.0-edge.4394f2ae",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"