@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.0

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.
Files changed (66) hide show
  1. package/bin/kumiko-schema-check.ts +7 -0
  2. package/package.json +12 -7
  3. package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
  4. package/src/__tests__/build-server-bundle.test.ts +62 -0
  5. package/src/__tests__/compose-stacks.test.ts +271 -0
  6. package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
  7. package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
  8. package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
  9. package/src/__tests__/discover-format.test.ts +1 -1
  10. package/src/__tests__/env-schema.integration.test.ts +1 -1
  11. package/src/__tests__/few-shot-corpus.test.ts +16 -11
  12. package/src/__tests__/merge-extra-context.test.ts +56 -0
  13. package/src/__tests__/resolve-stylesheet.test.ts +16 -0
  14. package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
  15. package/src/__tests__/scaffold-app-feature.test.ts +59 -6
  16. package/src/__tests__/scaffold-app.test.ts +34 -2
  17. package/src/__tests__/schema-apply.integration.test.ts +5 -2
  18. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
  19. package/src/__tests__/walkthrough.integration.test.ts +22 -6
  20. package/src/build-server-bundle.ts +33 -15
  21. package/src/build.ts +1 -2
  22. package/src/codegen/__tests__/render-codegen.test.ts +2 -1
  23. package/src/codegen/__tests__/run-codegen.test.ts +2 -2
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
  25. package/src/codegen/__tests__/watch.test.ts +1 -2
  26. package/src/codegen/render.ts +6 -1
  27. package/src/codegen/scan-events.ts +7 -5
  28. package/src/codegen/watch.ts +7 -4
  29. package/src/compose-stacks.ts +184 -0
  30. package/src/create-kumiko-server.ts +28 -5
  31. package/src/few-shot-corpus.ts +4 -3
  32. package/src/index.ts +30 -12
  33. package/src/run-dev-app.ts +195 -61
  34. package/src/scaffold-app-feature.ts +118 -15
  35. package/src/scaffold-app.ts +151 -79
  36. package/src/scaffold-demo-tasks.ts +233 -0
  37. package/src/schema-apply.ts +15 -2
  38. package/src/schema-check-core.ts +1 -1
  39. package/src/setup-test-stack-from-features.ts +61 -0
  40. package/src/welcome-banner.ts +1 -4
  41. package/src/__tests__/boot-extra-context.test.ts +0 -140
  42. package/src/__tests__/build-prod-bundle.test.ts +0 -311
  43. package/src/__tests__/cache-headers.test.ts +0 -83
  44. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
  45. package/src/__tests__/compose-features.test.ts +0 -129
  46. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  47. package/src/__tests__/inject-schema.test.ts +0 -62
  48. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  49. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  50. package/src/__tests__/require-env.test.ts +0 -29
  51. package/src/__tests__/resolve-auth-mail.test.ts +0 -69
  52. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  53. package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
  54. package/src/__tests__/run-prod-app-spec.test.ts +0 -57
  55. package/src/__tests__/run-prod-app.integration.test.ts +0 -840
  56. package/src/__tests__/session-wiring.test.ts +0 -51
  57. package/src/__tests__/try-hono-first.test.ts +0 -63
  58. package/src/boot/apply-boot-seeds.ts +0 -18
  59. package/src/build-prod-bundle.ts +0 -697
  60. package/src/compose-features.ts +0 -145
  61. package/src/extra-routes-deps.ts +0 -47
  62. package/src/inject-schema.ts +0 -24
  63. package/src/resolve-tailwind-cli.ts +0 -45
  64. package/src/run-prod-app.ts +0 -1492
  65. package/src/session-wiring.ts +0 -29
  66. package/src/try-hono-first.ts +0 -46
@@ -13,6 +13,9 @@ const SCAFFOLD_FILES = [
13
13
  "bunfig.toml",
14
14
  "bunfig.ci.toml",
15
15
  "src/run-config.ts",
16
+ "src/features/tasks/feature.ts",
17
+ "src/features/tasks/index.ts",
18
+ "src/seed.ts",
16
19
  "kumiko/schema.ts",
17
20
  "bin/main.ts",
18
21
  "bin/dev.ts",
@@ -89,6 +92,7 @@ describe("scaffoldApp", () => {
89
92
 
90
93
  const kumikoBin = readFileSync(join(dest, "bin/kumiko.ts"), "utf-8");
91
94
  expect(kumikoBin).toContain("runSchemaCli");
95
+ expect(kumikoBin).toContain("runConsumerCli");
92
96
  expect(kumikoBin).toContain("includeBundled: HAS_AUTH");
93
97
  });
94
98
 
@@ -112,9 +116,13 @@ describe("scaffoldApp", () => {
112
116
  expect(client).toContain("createKumikoApp");
113
117
  expect(client).toContain("emailPasswordClient");
114
118
  expect(client).toContain("DefaultAppShell");
115
- expect(client).toContain("shell: DefaultAppShell");
119
+ expect(client).toContain("shell: AppShell");
120
+ expect(client).toContain("function AppShell(");
121
+ expect(client).toContain("brand={<span");
122
+ expect(client).toContain(">my-shop</span>");
116
123
  expect(client).toContain('from "@cosmicdrift/kumiko-renderer-web"');
117
124
  expect(client).toContain('from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web"');
125
+ expect(client).toContain("tasksClient");
118
126
  });
119
127
 
120
128
  test(".env.example carries KUMIKO_DEV_DB_NAME default so reboots are persistent", async () => {
@@ -178,6 +186,27 @@ describe("scaffoldApp", () => {
178
186
  expect(readme).toContain("- `delivery`");
179
187
  });
180
188
 
189
+ test("callExpression/export-callability mismatch throws instead of silently mis-instantiating", async () => {
190
+ const dest = join(tmp, "my-shop");
191
+ await expect(
192
+ scaffoldApp({
193
+ name: "my-shop",
194
+ destination: dest,
195
+ features: [
196
+ {
197
+ name: "delivery",
198
+ importPath: "@cosmicdrift/kumiko-bundled-features/delivery",
199
+ exportName: "createDeliveryFeature",
200
+ // createDeliveryFeature is a factory function, but this claims it's
201
+ // a plain object export (no trailing "()") — must not silently push
202
+ // the function itself as a FeatureDefinition.
203
+ callExpression: "createDeliveryFeature",
204
+ },
205
+ ],
206
+ }),
207
+ ).rejects.toThrow(/is.*callable but callExpression/);
208
+ });
209
+
181
210
  test("bin/main.ts contains runProdApp + auth.admin stub + staticDir", async () => {
182
211
  const dest = join(tmp, "my-shop");
183
212
  await scaffoldApp({ name: "my-shop", destination: dest });
@@ -202,13 +231,15 @@ describe("scaffoldApp", () => {
202
231
  );
203
232
  });
204
233
 
205
- test("src/run-config.ts mounts secrets + sessions + HAS_AUTH", async () => {
234
+ test("src/run-config.ts mounts secrets + sessions + tasks + HAS_AUTH", async () => {
206
235
  const dest = join(tmp, "my-shop");
207
236
  await scaffoldApp({ name: "my-shop", destination: dest });
208
237
 
209
238
  const runConfig = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
210
239
  expect(runConfig).toContain("createSecretsFeature()");
211
240
  expect(runConfig).toContain("createSessionsFeature()");
241
+ expect(runConfig).toContain("tasksFeature");
242
+ expect(runConfig).toContain('from "./features/tasks"');
212
243
  expect(runConfig).toContain("export const APP_FEATURES");
213
244
  expect(runConfig).toContain("export const HAS_AUTH");
214
245
  });
@@ -362,6 +393,7 @@ describe("scaffoldApp", () => {
362
393
  const cfg = readFileSync(join(dest, "src/run-config.ts"), "utf-8");
363
394
  expect(cfg).not.toContain("createSecretsFeature");
364
395
  expect(cfg).not.toContain("createSessionsFeature");
396
+ expect(cfg).toContain("tasksFeature");
365
397
  });
366
398
 
367
399
  test("deterministic tenantId for same name (reproducible boots)", async () => {
@@ -6,7 +6,7 @@
6
6
  // `relation "kumiko_events" does not exist`. Dieser Test fährt den echten
7
7
  // Pfad gegen eine leere DB + den idempotenten Re-Run.
8
8
 
9
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9
+ import { afterAll, beforeAll, describe, expect, spyOn, test } from "bun:test";
10
10
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join } from "node:path";
@@ -76,7 +76,7 @@ describe("runSchemaApply", () => {
76
76
  expect(await tableExists(conn.db, "public.read_thing")).toBe(true);
77
77
  });
78
78
 
79
- test("rebuild-Marker für nicht-registrierte Tabelle → kein Crash, 0", async () => {
79
+ test("rebuild-Marker für nicht-registrierte Tabelle → kein Crash, 0, aber laut warnen (522/3)", async () => {
80
80
  writeFileSync(
81
81
  join(migDir, "0002_more.sql"),
82
82
  `CREATE TABLE "read_more" ("id" text PRIMARY KEY);`,
@@ -86,7 +86,10 @@ describe("runSchemaApply", () => {
86
86
  JSON.stringify({ version: 1, tables: ["read_more"] }),
87
87
  );
88
88
 
89
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
89
90
  expect(await runSchemaApply({ ...APPLY, appCwd })).toBe(0);
90
91
  expect(await tableExists(conn.db, "public.read_more")).toBe(true);
92
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('Table "read_more"'));
93
+ warn.mockRestore();
91
94
  });
92
95
  });
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { setupTestStackFromFeatures } from "../setup-test-stack-from-features";
4
+
5
+ const noopFeature = defineFeature("noop-app", () => {});
6
+
7
+ describe("setupTestStackFromFeatures", () => {
8
+ test("includeBundled prepends config/user/tenant/auth", async () => {
9
+ const stack = await setupTestStackFromFeatures([noopFeature], { includeBundled: true });
10
+ try {
11
+ for (const name of ["config", "user", "tenant", "auth-email-password", "noop-app"]) {
12
+ expect(stack.registry.getFeature(name)).toBeDefined();
13
+ }
14
+ } finally {
15
+ await stack.cleanup();
16
+ }
17
+ });
18
+
19
+ test("config preset wires configResolver in extraContext", async () => {
20
+ const stack = await setupTestStackFromFeatures([noopFeature], {
21
+ includeBundled: true,
22
+ presets: ["config"],
23
+ });
24
+ try {
25
+ expect(stack.registry.getFeature("config")).toBeDefined();
26
+ } finally {
27
+ await stack.cleanup();
28
+ }
29
+ });
30
+ });
@@ -13,10 +13,12 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
13
13
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
14
14
  import { tmpdir } from "node:os";
15
15
  import { join } from "node:path";
16
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
17
+ import { createPersonalAccessTokensFeature } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
16
18
  import { createSecretsFeature } from "@cosmicdrift/kumiko-bundled-features/secrets";
17
19
  import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
18
20
  import { createRegistry, defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
19
- import { composeFeatures } from "../compose-features";
21
+ import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
20
22
  import { scaffoldApp } from "../scaffold-app";
21
23
  import { scaffoldAppFeature } from "../scaffold-app-feature";
22
24
 
@@ -77,18 +79,32 @@ describe("walkthrough — DX-3.1 snapshot", () => {
77
79
  // a dummy defineFeature here — the scaffold-side of "notesFeature"
78
80
  // (file-content) is pinned in test 2; this test pins the runtime-side
79
81
  // (composeFeatures auto-prepend behaviour the walkthrough claims).
82
+ //
83
+ // sessions now requires auth-foundation (#1370/#1371), which itself
84
+ // needs a tokenVerifier provider mounted (#1368) — PAT here, mirroring
85
+ // the scaffold picker's real-world default. Not part of the walkthrough's
86
+ // advertised 7-feature count (that count is about includeBundled's
87
+ // auto-mount, unrelated to this coupling), so both are added on top.
80
88
  const notesFeature = defineFeature("notes", () => {});
81
- const APP_FEATURES = [createSecretsFeature(), createSessionsFeature(), notesFeature];
89
+ const APP_FEATURES = [
90
+ createSecretsFeature(),
91
+ authFoundationFeature,
92
+ createPersonalAccessTokensFeature({ scopes: {} }),
93
+ createSessionsFeature(),
94
+ notesFeature,
95
+ ];
82
96
 
83
97
  const composed = composeFeatures(APP_FEATURES, { includeBundled: true });
84
- // 3 explicit + 4 auto-mounted bundled = 7 total features.
85
- expect(composed.length).toBe(7);
98
+ // 5 explicit + 4 auto-mounted bundled = 9 total features.
99
+ expect(composed.length).toBe(9);
86
100
 
87
101
  const composedNames = composed.map((f) => f.name).sort();
88
102
  expect(composedNames).toEqual([
89
103
  "auth-email-password",
104
+ "auth-foundation",
90
105
  "config",
91
106
  "notes",
107
+ "personal-access-tokens",
92
108
  "secrets",
93
109
  "sessions",
94
110
  "tenant",
@@ -97,9 +113,9 @@ describe("walkthrough — DX-3.1 snapshot", () => {
97
113
 
98
114
  // validateBoot must pass (no missing-requires, no schema-errors).
99
115
  expect(() => validateBoot(composed)).not.toThrow();
100
- // Registry must contain all 7 features.
116
+ // Registry must contain all 9 features.
101
117
  const registry = createRegistry(composed);
102
- expect(registry.features.size).toBe(7);
118
+ expect(registry.features.size).toBe(9);
103
119
  });
104
120
 
105
121
  test("bin/main.ts contains the auth.admin stub the walkthrough relies on", async () => {
@@ -38,6 +38,7 @@
38
38
  import { existsSync, readFileSync, statSync } from "node:fs";
39
39
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
40
40
  import { dirname, join, resolve } from "node:path";
41
+ import { parseJsonOrThrow, parseJsonSafe } from "@cosmicdrift/kumiko-framework/utils";
41
42
 
42
43
  const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
43
44
 
@@ -46,6 +47,12 @@ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
46
47
  // ioredis), dynamic-require (postgres, temporal-polyfill) — alles was unter
47
48
  // Bun-bundling bricht. (drizzle-kit/drizzle-orm sind raus: der Migrate-Pfad
48
49
  // nutzt jetzt den framework-eigenen runMigrationsFromDir, kein drizzle-kit.)
50
+ // meilisearch: deliberately here, not in BUILD_ONLY_EXTERNALS below — apps
51
+ // that import createMeilisearchAdapter (kumiko-framework/search/meilisearch)
52
+ // reference the package at runtime (no native binding, plain HTTP client,
53
+ // but a real runtime import). Found 2026-07-19: money-horse prod crash
54
+ // "Cannot find package 'meilisearch'" because it used to sit in
55
+ // BUILD_ONLY_EXTERNALS instead.
49
56
  const RUNTIME_EXTERNALS = [
50
57
  "@node-rs/argon2",
51
58
  "bullmq",
@@ -54,6 +61,7 @@ const RUNTIME_EXTERNALS = [
54
61
  "temporal-polyfill",
55
62
  "pino",
56
63
  "pino-pretty",
64
+ "meilisearch",
57
65
  ] as const;
58
66
 
59
67
  // Pakete die nur im Build-Stack erscheinen (transitive Imports im Framework),
@@ -62,7 +70,6 @@ const RUNTIME_EXTERNALS = [
62
70
  // Bundle — der Marker schaltet nur das resolution-during-build ab. NICHT in
63
71
  // runtime-deps.
64
72
  const BUILD_ONLY_EXTERNALS = [
65
- "meilisearch",
66
73
  "@planetscale/database",
67
74
  "@libsql/client",
68
75
  "better-sqlite3",
@@ -178,7 +185,7 @@ export async function buildServerBundle(
178
185
  entries.sort((a, b) => a.file.localeCompare(b.file));
179
186
  const totalBytes = [...entries, ...chunks].reduce((sum, e) => sum + e.sizeBytes, 0);
180
187
 
181
- const runtimeDeps = await resolveRuntimeDepsVersions(repoRoot, [
188
+ const runtimeDeps = await resolveRuntimeDepsVersions(cwd, repoRoot, [
182
189
  ...RUNTIME_EXTERNALS,
183
190
  ...(options.extraRuntimeExternals ?? []),
184
191
  ]);
@@ -217,29 +224,35 @@ function findRepoRoot(start: string): string | undefined {
217
224
  return undefined;
218
225
  }
219
226
 
220
- // Versionen für RUNTIME_EXTERNALS auflösen: erst aus framework + bundled-
221
- // features package.json (Workspace-pinning), Fallback auf "*" wenn nichts
222
- // gefunden. Ein Repo-Root muss existieren sonst kann eh kein CLI-Bundle
223
- // gemacht werden, also wird die Funktion gar nicht erst gerufen.
227
+ // Versionen für RUNTIME_EXTERNALS auflösen: erst aus den installierten
228
+ // node_modules/@cosmicdrift/*-Packages (relativ zu cwd funktioniert für
229
+ // jede Consumer-App), dann als Zusatz aus packages/framework + bundled-
230
+ // features (Workspace-Checkout des Frameworks selbst). Fallback auf "*"
231
+ // wenn keine Quelle den Namen kennt.
224
232
  async function resolveRuntimeDepsVersions(
233
+ cwd: string,
225
234
  repoRoot: string | undefined,
226
235
  packages: readonly string[],
227
236
  ): Promise<Record<string, string>> {
228
237
  const out: Record<string, string> = {};
229
- if (!repoRoot) {
230
- for (const pkg of packages) out[pkg] = "*";
231
- return out;
232
- }
233
238
 
234
239
  const pinSources = [
235
- join(repoRoot, "packages/framework/package.json"),
236
- join(repoRoot, "packages/bundled-features/package.json"),
240
+ ...(repoRoot
241
+ ? [
242
+ join(repoRoot, "packages/framework/package.json"),
243
+ join(repoRoot, "packages/bundled-features/package.json"),
244
+ ]
245
+ : []),
246
+ // node_modules applied last so its versions win over the repo-root
247
+ // fallback — see the function comment above (#1217 bug class).
248
+ join(cwd, "node_modules/@cosmicdrift/kumiko-framework/package.json"),
249
+ join(cwd, "node_modules/@cosmicdrift/kumiko-bundled-features/package.json"),
237
250
  ];
238
251
  const allDeps: Record<string, string> = {};
239
252
  for (const path of pinSources) {
240
253
  if (!existsSync(path)) continue;
241
254
  const raw = await readFile(path, "utf-8");
242
- const parsed = JSON.parse(raw) as { dependencies?: Record<string, string> };
255
+ const parsed = parseJsonOrThrow<{ dependencies?: Record<string, string> }>(raw, path);
243
256
  Object.assign(allDeps, parsed.dependencies ?? {});
244
257
  }
245
258
  for (const pkg of packages) {
@@ -251,12 +264,17 @@ async function resolveRuntimeDepsVersions(
251
264
  function derivePkgName(cwd: string): string {
252
265
  const pkgJson = join(cwd, "package.json");
253
266
  if (!existsSync(pkgJson)) return "kumiko-app-runtime";
267
+ // existsSync/readFileSync race (deleted between the two calls) or a
268
+ // permission error must fall back like a missing file, not crash the
269
+ // build — read here too, not just the JSON.parse below.
270
+ let raw: string;
254
271
  try {
255
- const parsed = JSON.parse(readFileSync(pkgJson, "utf-8")) as { name?: string };
256
- return parsed.name ? `${parsed.name}-runtime` : "kumiko-app-runtime";
272
+ raw = readFileSync(pkgJson, "utf-8");
257
273
  } catch {
258
274
  return "kumiko-app-runtime";
259
275
  }
276
+ const parsed = parseJsonSafe<{ name?: string }>(raw, {});
277
+ return parsed.name ? `${parsed.name}-runtime` : "kumiko-app-runtime";
260
278
  }
261
279
 
262
280
  export function formatServerBuildResult(
package/src/build.ts CHANGED
@@ -10,11 +10,10 @@ export {
10
10
  buildProdBundle,
11
11
  type ClientEntry,
12
12
  discoverClientEntries,
13
- discoverClientEntry,
14
13
  discoverHtmlTemplate,
15
14
  formatBuildResult,
16
15
  injectAssetTags,
17
- } from "./build-prod-bundle";
16
+ } from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
18
17
 
19
18
  export {
20
19
  type BuildServerBundleEntry,
@@ -79,7 +79,8 @@ describe("renderDefineFile", () => {
79
79
  const out = renderDefineFile(["tenant:write:create", "tenant:write:update"]);
80
80
  expect(out).toContain("export type TypedDispatcher");
81
81
  expect(out).toContain("export function createTypedDispatcher");
82
- expect(out).toContain('export type { WriteHandlerQn } from "./types.generated"');
82
+ expect(out).toContain('import type { WriteHandlerQn } from "./types.generated"');
83
+ expect(out).toContain("export type { WriteHandlerQn };");
83
84
  });
84
85
  });
85
86
 
@@ -283,7 +283,7 @@ export default defineFeature("dup", (r) => {
283
283
  const result = runCodegen({ appRoot });
284
284
  expect(result.eventCount).toBe(1);
285
285
  expect(result.warnings.length).toBeGreaterThanOrEqual(1);
286
- const w = result.warnings.find((w) => w.reason.includes("duplicate"));
286
+ const w = result.warnings.find((w) => w.message.includes("duplicate"));
287
287
  expect(w).toBeDefined();
288
288
  });
289
289
 
@@ -368,7 +368,7 @@ export default defineFeature("inline", (r) => {
368
368
  const result = runCodegen({ appRoot });
369
369
  expect(result.eventCount).toBe(0);
370
370
  expect(result.warnings.length).toBe(1);
371
- expect(result.warnings[0]?.reason).toMatch(/not a named import nor an inline z\.\* call/);
371
+ expect(result.warnings[0]?.message).toMatch(/not a named import nor an inline z\.\* call/);
372
372
  });
373
373
 
374
374
  test("skips test files, node_modules, .kumiko, dist", () => {
@@ -1,7 +1,12 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import * as ts from "typescript";
4
+ // TS7's package.json exports only version.cjs at ".", so the classic
5
+ // createProgram/ScriptTarget Compiler API is unreachable via `import *
6
+ // as ts from "typescript"` anymore; ts-morph's vendored (older) compiler
7
+ // build is the only way to get that surface, so diagnostics here run
8
+ // against a different compiler than the repo's own tsc.
9
+ import { ts } from "ts-morph";
5
10
  import { runCodegen } from "../run-codegen";
6
11
 
7
12
  const REPO_ROOT = join(__dirname, "../../../../..");
@@ -138,8 +138,7 @@ export default defineFeature("orders", (r) => {
138
138
  writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("nope", "evt"));
139
139
  const handle = watchAndRegenerate({ appRoot, onResult: () => {} });
140
140
  handle.close();
141
- handle.close(); // must not throw
142
- expect(true).toBe(true);
141
+ expect(() => handle.close()).not.toThrow();
143
142
  });
144
143
 
145
144
  test("non-ts file changes do not trigger codegen", async () => {
@@ -175,8 +175,13 @@ export function renderDefineFile(handlerQns: readonly string[] = []): string {
175
175
  ? [
176
176
  "",
177
177
  `import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`,
178
+ `import type { WriteHandlerQn } from "./types.generated";`,
178
179
  "",
179
- `export type { WriteHandlerQn } from "./types.generated";`,
180
+ // Plain `export type { WriteHandlerQn } from "./types.generated"`
181
+ // only re-exports — with verbatimModuleSyntax it does NOT bind the
182
+ // name locally, so the `TypedDispatcher` reference below would fail
183
+ // to resolve. Import above + bare re-export here does both.
184
+ `export type { WriteHandlerQn };`,
180
185
  "",
181
186
  `/** Dispatcher with write() narrowed to registered handler QNs. */`,
182
187
  `export type TypedDispatcher = Omit<Dispatcher, "write"> & {`,
@@ -93,7 +93,7 @@ export type SchemaSource =
93
93
  export type ScanWarning = {
94
94
  readonly file: string;
95
95
  readonly line: number;
96
- readonly reason: string;
96
+ readonly message: string;
97
97
  };
98
98
 
99
99
  export type ScanResult = {
@@ -152,7 +152,7 @@ function collectTsFiles(dir: string, out: string[]): void {
152
152
  try {
153
153
  entries = readdirSync(dir);
154
154
  } catch {
155
- // Directory missing — fine, just no files to scan there.
155
+ // skip: directory unreadable, nothing to scan there
156
156
  return;
157
157
  }
158
158
  for (const entry of entries) {
@@ -224,8 +224,9 @@ function collectFromDefineEvent(
224
224
  warnings.push({
225
225
  file: filePath,
226
226
  line: call.getStartLineNumber(),
227
- reason: "r.defineEvent: cannot read event-name + schema statically",
227
+ message: "r.defineEvent: cannot read event-name + schema statically",
228
228
  });
229
+ // skip: defineEvent call shape not statically parseable, already recorded as warning
229
230
  return;
230
231
  }
231
232
 
@@ -241,8 +242,9 @@ function collectFromDefineEvent(
241
242
  warnings.push({
242
243
  file: filePath,
243
244
  line: call.getStartLineNumber(),
244
- reason: `r.defineEvent("${parsed.eventName}"): schema "${parsed.schemaNode.getText()}" — not a named import nor an inline z.* call, skipped`,
245
+ message: `r.defineEvent("${parsed.eventName}"): schema "${parsed.schemaNode.getText()}" — not a named import nor an inline z.* call, skipped`,
245
246
  });
247
+ // skip: schema source not resolvable, already recorded as warning
246
248
  return;
247
249
  }
248
250
 
@@ -544,7 +546,7 @@ function dedupe(events: ScannedEvent[], warnings: ScanWarning[]): ScannedEvent[]
544
546
  warnings.push({
545
547
  file: ev.source.file,
546
548
  line: ev.source.line,
547
- reason: `duplicate r.defineEvent("${ev.qualifiedName}") — first declared at ${existing.source.file}:${existing.source.line}, ignored here`,
549
+ message: `duplicate r.defineEvent("${ev.qualifiedName}") — first declared at ${existing.source.file}:${existing.source.line}, ignored here`,
548
550
  });
549
551
  continue;
550
552
  }
@@ -74,7 +74,7 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
74
74
  if (result.warnings.length > 0) {
75
75
  for (const w of result.warnings) {
76
76
  // biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
77
- console.warn(`[codegen] ${w.file}:${w.line} — ${w.reason}`);
77
+ console.warn(`[codegen] ${w.file}:${w.line} — ${w.message}`);
78
78
  }
79
79
  }
80
80
  }
@@ -95,17 +95,19 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
95
95
 
96
96
  try {
97
97
  watcher = watch(srcDir, { recursive: true }, (_eventType, filename) => {
98
+ // skip: watcher closed or no filename reported, nothing to act on
98
99
  if (closed || !filename) return;
99
100
  // node liefert filename relativ zu srcDir, kann aber posix oder
100
101
  // windows-style separators haben. Wir prüfen substring-tolerant.
101
102
  const normalised = `/${filename.toString().replace(/\\/g, "/")}`;
103
+ // skip: path matches an excluded segment (node_modules/dist/etc.)
102
104
  if (SKIP_SUBSTRINGS.some((seg) => normalised.includes(seg))) return;
103
- // Nur .ts/.tsx interessieren — alles andere (CSS, MD, JSON) hat
105
+ // skip: Nur .ts/.tsx interessieren — alles andere (CSS, MD, JSON) hat
104
106
  // keinen Einfluss auf r.defineEvent-Calls.
105
107
  if (!normalised.endsWith(".ts") && !normalised.endsWith(".tsx")) return;
106
- // .d.ts ausgenommen — die kommen meistens vom codegen selbst.
108
+ // skip: .d.ts ausgenommen — die kommen meistens vom codegen selbst.
107
109
  if (normalised.endsWith(".d.ts")) return;
108
- // .test.ts/.test.tsx ausgenommen — Tests definieren keine
110
+ // skip: .test.ts/.test.tsx ausgenommen — Tests definieren keine
109
111
  // Production-Features.
110
112
  if (normalised.endsWith(".test.ts") || normalised.endsWith(".test.tsx")) return;
111
113
 
@@ -127,6 +129,7 @@ export function watchAndRegenerate(opts: WatchOptions): WatchHandle {
127
129
 
128
130
  return {
129
131
  close: () => {
132
+ // skip: already closed, avoid double-close
130
133
  if (closed) return;
131
134
  closed = true;
132
135
  if (timer) clearTimeout(timer);