@genroc/eval-node 0.0.0-edge.0477fb0 → 0.0.0-edge.04ed705c

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