@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.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 (125) hide show
  1. package/README.md +17 -0
  2. package/bin/kumiko-build.ts +3 -2
  3. package/bin/kumiko-dev.ts +1 -1
  4. package/bin/kumiko-init-deploy.ts +1 -1
  5. package/bin/kumiko-schema-check.ts +4 -1
  6. package/dist/build-server-bundle.d.ts +31 -0
  7. package/dist/build-server-bundle.js +282 -0
  8. package/dist/build.d.ts +2 -0
  9. package/dist/build.js +5 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/codegen/index.d.ts +4 -0
  13. package/dist/codegen/index.js +4 -0
  14. package/dist/codegen/render.d.ts +55 -0
  15. package/dist/codegen/render.js +273 -0
  16. package/dist/codegen/run-codegen.d.ts +20 -0
  17. package/dist/codegen/run-codegen.js +219 -0
  18. package/dist/codegen/scan-events.d.ts +76 -0
  19. package/dist/codegen/scan-events.js +447 -0
  20. package/dist/codegen/watch.d.ts +33 -0
  21. package/dist/codegen/watch.js +112 -0
  22. package/dist/compose-stacks.d.ts +60 -0
  23. package/dist/compose-stacks.js +127 -0
  24. package/dist/crash-tracker.d.ts +17 -0
  25. package/dist/crash-tracker.js +37 -0
  26. package/dist/create-kumiko-server.d.ts +195 -0
  27. package/dist/create-kumiko-server.js +1040 -0
  28. package/dist/env-schema.d.ts +15 -0
  29. package/dist/env-schema.js +60 -0
  30. package/dist/few-shot-corpus.d.ts +72 -0
  31. package/dist/few-shot-corpus.js +262 -0
  32. package/dist/framework-version.d.ts +1 -0
  33. package/dist/framework-version.js +31 -0
  34. package/dist/index.d.ts +22 -0
  35. package/dist/index.js +33 -0
  36. package/dist/init-deploy-cli.d.ts +10 -0
  37. package/dist/init-deploy-cli.js +111 -0
  38. package/dist/kebab.d.ts +1 -0
  39. package/{src/kebab.ts → dist/kebab.js} +2 -3
  40. package/dist/run-dev-app.d.ts +174 -0
  41. package/dist/run-dev-app.js +403 -0
  42. package/dist/scaffold-app-feature.d.ts +16 -0
  43. package/dist/scaffold-app-feature.js +240 -0
  44. package/dist/scaffold-app.d.ts +45 -0
  45. package/dist/scaffold-app.js +852 -0
  46. package/dist/scaffold-demo-tasks.d.ts +8 -0
  47. package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
  48. package/dist/scaffold-deploy.d.ts +92 -0
  49. package/dist/scaffold-deploy.js +254 -0
  50. package/dist/scaffold-feature.d.ts +26 -0
  51. package/dist/scaffold-feature.js +163 -0
  52. package/dist/schema-apply.d.ts +11 -0
  53. package/dist/schema-apply.js +95 -0
  54. package/dist/schema-check-core.d.ts +2 -0
  55. package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
  56. package/dist/schema-tables-auth-mode.d.ts +2 -0
  57. package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
  58. package/dist/schema-tables-minimal.d.ts +2 -0
  59. package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
  60. package/dist/setup-test-stack-from-features.d.ts +11 -0
  61. package/dist/setup-test-stack-from-features.js +42 -0
  62. package/dist/welcome-banner.d.ts +15 -0
  63. package/dist/welcome-banner.js +27 -0
  64. package/package.json +71 -24
  65. package/src/changes.json +6 -0
  66. package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
  67. package/src/__tests__/build-server-bundle.test.ts +0 -228
  68. package/src/__tests__/classify-change.test.ts +0 -87
  69. package/src/__tests__/compose-stacks.test.ts +0 -273
  70. package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
  71. package/src/__tests__/crash-tracker.test.ts +0 -89
  72. package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
  73. package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
  74. package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
  75. package/src/__tests__/discover-format.test.ts +0 -32
  76. package/src/__tests__/env-schema.integration.test.ts +0 -248
  77. package/src/__tests__/env-schema.test.ts +0 -132
  78. package/src/__tests__/few-shot-corpus.test.ts +0 -316
  79. package/src/__tests__/framework-version.test.ts +0 -45
  80. package/src/__tests__/init-deploy-cli.test.ts +0 -113
  81. package/src/__tests__/kumiko-schema-check.test.ts +0 -56
  82. package/src/__tests__/merge-extra-context.test.ts +0 -67
  83. package/src/__tests__/resolve-stylesheet.test.ts +0 -132
  84. package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
  85. package/src/__tests__/run-dev-app.integration.test.ts +0 -290
  86. package/src/__tests__/runtime-classification.test.ts +0 -26
  87. package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
  88. package/src/__tests__/scaffold-app-feature.test.ts +0 -216
  89. package/src/__tests__/scaffold-app.test.ts +0 -630
  90. package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
  91. package/src/__tests__/scaffold-deploy.test.ts +0 -464
  92. package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
  93. package/src/__tests__/scaffold-feature.test.ts +0 -143
  94. package/src/__tests__/schema-apply.integration.test.ts +0 -191
  95. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
  96. package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
  97. package/src/__tests__/walkthrough.integration.test.ts +0 -140
  98. package/src/__tests__/welcome-banner.test.ts +0 -42
  99. package/src/build-server-bundle.ts +0 -346
  100. package/src/build.ts +0 -28
  101. package/src/codegen/__tests__/render-codegen.test.ts +0 -100
  102. package/src/codegen/__tests__/run-codegen.test.ts +0 -665
  103. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
  104. package/src/codegen/__tests__/watch.test.ts +0 -222
  105. package/src/codegen/index.ts +0 -19
  106. package/src/codegen/render.ts +0 -305
  107. package/src/codegen/run-codegen.ts +0 -278
  108. package/src/codegen/scan-events.ts +0 -565
  109. package/src/codegen/watch.ts +0 -140
  110. package/src/compose-stacks.ts +0 -193
  111. package/src/crash-tracker.ts +0 -56
  112. package/src/create-kumiko-server.ts +0 -1397
  113. package/src/env-schema.ts +0 -72
  114. package/src/few-shot-corpus.ts +0 -370
  115. package/src/framework-version.ts +0 -39
  116. package/src/index.ts +0 -105
  117. package/src/init-deploy-cli.ts +0 -148
  118. package/src/run-dev-app.ts +0 -694
  119. package/src/scaffold-app-feature.ts +0 -283
  120. package/src/scaffold-app.ts +0 -1033
  121. package/src/scaffold-deploy.ts +0 -393
  122. package/src/scaffold-feature.ts +0 -226
  123. package/src/schema-apply.ts +0 -123
  124. package/src/setup-test-stack-from-features.ts +0 -80
  125. package/src/welcome-banner.ts +0 -43
@@ -1,347 +0,0 @@
1
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { dirname, join } from "node:path";
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";
10
- import { runCodegen } from "../run-codegen";
11
-
12
- const REPO_ROOT = join(__dirname, "../../../../..");
13
- const FRAMEWORK_SRC = join(REPO_ROOT, "packages/framework/src");
14
-
15
- const TEST_FIXTURE_DIR = join(__dirname, ".tmp-fixtures");
16
- const createdDirs: string[] = [];
17
-
18
- function makeAppDir(): string {
19
- mkdirSync(TEST_FIXTURE_DIR, { recursive: true });
20
- const dir = mkdtempSync(join(TEST_FIXTURE_DIR, "app-"));
21
- createdDirs.push(dir);
22
- return dir;
23
- }
24
-
25
- afterAll(() => {
26
- for (const d of createdDirs) {
27
- try {
28
- rmSync(d, { recursive: true, force: true });
29
- } catch {}
30
- }
31
- try {
32
- rmSync(TEST_FIXTURE_DIR, { recursive: true, force: true });
33
- } catch {}
34
- });
35
-
36
- function write(dir: string, relPath: string, content: string): string {
37
- const full = join(dir, relPath);
38
- mkdirSync(dirname(full), { recursive: true });
39
- writeFileSync(full, content, "utf-8");
40
- return full;
41
- }
42
-
43
- function compileApp(appRoot: string): readonly ts.Diagnostic[] {
44
- const compilerOptions: ts.CompilerOptions = {
45
- target: ts.ScriptTarget.ESNext,
46
- module: ts.ModuleKind.ESNext,
47
- moduleResolution: ts.ModuleResolutionKind.Bundler,
48
- strict: true,
49
- skipLibCheck: true,
50
- esModuleInterop: true,
51
- noEmit: true,
52
- baseUrl: REPO_ROOT,
53
- paths: {
54
- "@cosmicdrift/kumiko-framework/*": [join(FRAMEWORK_SRC, "*/index.ts")],
55
- },
56
- types: [],
57
- };
58
-
59
- const program = ts.createProgram({
60
- rootNames: collectFiles(appRoot),
61
- options: compilerOptions,
62
- });
63
- return ts.getPreEmitDiagnostics(program);
64
- }
65
-
66
- function collectFiles(dir: string): string[] {
67
- const out: string[] = [];
68
- const fs = require("node:fs");
69
- const walk = (d: string) => {
70
- let entries: string[];
71
- try {
72
- entries = fs.readdirSync(d);
73
- } catch {
74
- return;
75
- }
76
- for (const e of entries) {
77
- if (e.startsWith(".") && e !== ".kumiko") continue;
78
- if (e === "node_modules") continue;
79
- const full = join(d, e);
80
- const stat = fs.statSync(full);
81
- if (stat.isDirectory()) walk(full);
82
- else if (stat.isFile() && (e.endsWith(".ts") || e.endsWith(".tsx")) && !e.endsWith(".d.ts"))
83
- out.push(full);
84
- }
85
- };
86
- walk(join(dir, "src"));
87
- walk(join(dir, ".kumiko"));
88
- return out;
89
- }
90
-
91
- function writeOrderPlacedSchema(appRoot: string): void {
92
- write(
93
- appRoot,
94
- "src/feature/events.ts",
95
- [
96
- 'import { z } from "zod";',
97
- "export const orderPlacedSchema = z.object({",
98
- " orderId: z.string(),",
99
- " customerId: z.string(),",
100
- " amount: z.number(),",
101
- "});",
102
- "",
103
- ].join("\n"),
104
- );
105
- }
106
-
107
- describe("strict-mode diagnostics -- the actual contract of the codegen", () => {
108
- let appRoot: string;
109
- let allDiagnostics: readonly ts.Diagnostic[];
110
-
111
- beforeAll(() => {
112
- appRoot = makeAppDir();
113
- writeOrderPlacedSchema(appRoot);
114
- write(
115
- appRoot,
116
- "src/feature/feature.ts",
117
- [
118
- 'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
119
- 'import { orderPlacedSchema } from "./events";',
120
- "",
121
- 'export const ordersFeature = defineFeature("orders", (r) => ({',
122
- ' placed: r.defineEvent("placed", orderPlacedSchema),',
123
- "}));",
124
- "",
125
- ].join("\n"),
126
- );
127
- runCodegen({ appRoot });
128
-
129
- write(
130
- appRoot,
131
- "src/feature/handler-good.ts",
132
- [
133
- 'import { defineWriteHandler } from "../../.kumiko/define";',
134
- 'import { z } from "zod";',
135
- "",
136
- "export const placeOrder = defineWriteHandler({",
137
- ' name: "orders.placeOrder",',
138
- " schema: z.object({}),",
139
- ' access: { roles: ["Admin"] },',
140
- " handler: async (_event, ctx) => {",
141
- " await ctx.appendEvent({",
142
- ' aggregateId: "x",',
143
- ' aggregateType: "order",',
144
- ' type: "orders:event:placed",',
145
- ' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
146
- " });",
147
- ' return { isSuccess: true as const, data: { id: "o1" } };',
148
- " },",
149
- "});",
150
- "",
151
- ].join("\n"),
152
- );
153
-
154
- write(
155
- appRoot,
156
- "src/feature/handler-unknown-type.ts",
157
- [
158
- 'import { defineWriteHandler } from "../../.kumiko/define";',
159
- 'import { z } from "zod";',
160
- "",
161
- "export const placeOrder = defineWriteHandler({",
162
- ' name: "orders.placeOrder",',
163
- " schema: z.object({}),",
164
- ' access: { roles: ["Admin"] },',
165
- " handler: async (_event, ctx) => {",
166
- " await ctx.appendEvent({",
167
- ' aggregateId: "x",',
168
- ' aggregateType: "order",',
169
- ' type: "totally:made:up",',
170
- " payload: { whatever: 1 },",
171
- " });",
172
- ' return { isSuccess: true as const, data: { id: "x" } };',
173
- " },",
174
- "});",
175
- "",
176
- ].join("\n"),
177
- );
178
-
179
- write(
180
- appRoot,
181
- "src/feature/handler-payload-mismatch.ts",
182
- [
183
- 'import { defineWriteHandler } from "../../.kumiko/define";',
184
- 'import { z } from "zod";',
185
- "",
186
- "export const placeOrder = defineWriteHandler({",
187
- ' name: "orders.placeOrder",',
188
- " schema: z.object({}),",
189
- ' access: { roles: ["Admin"] },',
190
- " handler: async (_event, ctx) => {",
191
- " await ctx.appendEvent({",
192
- ' aggregateId: "x",',
193
- ' aggregateType: "order",',
194
- ' type: "orders:event:placed",',
195
- ' payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },',
196
- " });",
197
- ' return { isSuccess: true as const, data: { id: "o1" } };',
198
- " },",
199
- "});",
200
- "",
201
- ].join("\n"),
202
- );
203
-
204
- write(
205
- appRoot,
206
- "src/feature/handler-direct.ts",
207
- [
208
- 'import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";',
209
- 'import { z } from "zod";',
210
- "",
211
- "export const placeOrder = defineWriteHandler({",
212
- ' name: "orders.placeOrder",',
213
- " schema: z.object({}),",
214
- ' access: { roles: ["Admin"] },',
215
- " handler: async (_event, ctx) => {",
216
- " await ctx.appendEvent({",
217
- ' aggregateId: "x",',
218
- ' aggregateType: "order",',
219
- ' type: "orders:event:placed",',
220
- ' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
221
- " });",
222
- ' return { isSuccess: true as const, data: { id: "o1" } };',
223
- " },",
224
- "});",
225
- "",
226
- ].join("\n"),
227
- );
228
-
229
- write(
230
- appRoot,
231
- "src/feature/handler-byname-good.ts",
232
- [
233
- 'import { defineWriteHandler } from "../../.kumiko/define";',
234
- 'import { z } from "zod";',
235
- 'import { ordersFeature } from "./feature";',
236
- "",
237
- "const { placed } = ordersFeature.exports;",
238
- "",
239
- "export const placeOrder = defineWriteHandler({",
240
- ' name: "orders.placeOrder",',
241
- " schema: z.object({}),",
242
- ' access: { roles: ["Admin"] },',
243
- " handler: async (_event, ctx) => {",
244
- " await ctx.appendEvent({",
245
- ' aggregateId: "x",',
246
- ' aggregateType: "order",',
247
- " type: placed.name,",
248
- ' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
249
- " });",
250
- ' return { isSuccess: true as const, data: { id: "o1" } };',
251
- " },",
252
- "});",
253
- "",
254
- ].join("\n"),
255
- );
256
-
257
- write(
258
- appRoot,
259
- "src/feature/handler-byname-bad.ts",
260
- [
261
- 'import { defineWriteHandler } from "../../.kumiko/define";',
262
- 'import { z } from "zod";',
263
- 'import { ordersFeature } from "./feature";',
264
- "",
265
- "const { placed } = ordersFeature.exports;",
266
- "",
267
- "export const placeOrder = defineWriteHandler({",
268
- ' name: "orders.placeOrder",',
269
- " schema: z.object({}),",
270
- ' access: { roles: ["Admin"] },',
271
- " handler: async (_event, ctx) => {",
272
- " await ctx.appendEvent({",
273
- ' aggregateId: "x",',
274
- ' aggregateType: "order",',
275
- " type: placed.name,",
276
- ' payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },',
277
- " });",
278
- ' return { isSuccess: true as const, data: { id: "o1" } };',
279
- " },",
280
- "});",
281
- "",
282
- ].join("\n"),
283
- );
284
-
285
- allDiagnostics = compileApp(appRoot);
286
- }, 120_000);
287
-
288
- test("good ctx.appendEvent compiles cleanly", () => {
289
- const handlerErrors = allDiagnostics.filter((d) =>
290
- d.file?.fileName.endsWith("/feature/handler-good.ts"),
291
- );
292
- expect(handlerErrors).toHaveLength(0);
293
- });
294
-
295
- test("unknown event-type triggers TS2322 with augmented map in error message", () => {
296
- const handlerErrors = allDiagnostics.filter((d) =>
297
- d.file?.fileName.endsWith("/feature/handler-unknown-type.ts"),
298
- );
299
- const ts2322 = handlerErrors.filter((d) => d.code === 2322);
300
- expect(ts2322.length).toBeGreaterThan(0);
301
- const flattened = ts2322
302
- .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"))
303
- .join("\n");
304
- expect(flattened).toMatch(/keyof KumikoEventTypeMap|"orders:event:placed"/);
305
- });
306
-
307
- test("payload-shape mismatch triggers a property-error", () => {
308
- const handlerErrors = allDiagnostics.filter((d) =>
309
- d.file?.fileName.endsWith("/feature/handler-payload-mismatch.ts"),
310
- );
311
- const propErrors = handlerErrors.filter((d) => d.code === 2353);
312
- expect(propErrors.length).toBeGreaterThan(0);
313
- const flattened = propErrors
314
- .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"))
315
- .join("\n");
316
- expect(flattened).toMatch(/'bogus'/);
317
- });
318
-
319
- test("direct framework-import + augmentation-included compiles strict too", () => {
320
- const handlerErrors = allDiagnostics.filter((d) =>
321
- d.file?.fileName.endsWith("/feature/handler-direct.ts"),
322
- );
323
- expect(handlerErrors).toHaveLength(0);
324
- });
325
-
326
- test("eventDef.name pattern: literal-typed name resolves to correct payload-shape", () => {
327
- const goodErrors = allDiagnostics.filter((d) =>
328
- d.file?.fileName.endsWith("/feature/handler-byname-good.ts"),
329
- );
330
- if (goodErrors.length > 0) {
331
- const msgs = goodErrors
332
- .map((d) => ` TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`)
333
- .join("\n");
334
- throw new Error(`expected handler-byname-good.ts to compile cleanly, got:\n${msgs}`);
335
- }
336
-
337
- const badErrors = allDiagnostics.filter((d) =>
338
- d.file?.fileName.endsWith("/feature/handler-byname-bad.ts"),
339
- );
340
- const propErrors = badErrors.filter((d) => d.code === 2353);
341
- expect(propErrors.length).toBeGreaterThan(0);
342
- const flattened = propErrors
343
- .map((d) => ts.flattenDiagnosticMessageText(d.messageText, "\n"))
344
- .join("\n");
345
- expect(flattened).toMatch(/'bogus'/);
346
- });
347
- });
@@ -1,222 +0,0 @@
1
- // watchAndRegenerate — file-watcher-Tests. Verifiziert: initial-Pass
2
- // läuft synchron, file-changes triggern einen erneuten Pass mit Debounce,
3
- // close() ist idempotent.
4
- //
5
- // Fixtures liegen wie bei strict-mode-diagnostics.test.ts unter
6
- // `__tests__/.tmp-fixtures/` (gitignored), damit Node's natürliches
7
- // `node_modules`-Hochsuchen 'zod' findet — auch wenn watch-Tests
8
- // 'zod' nicht direct nutzen, runCodegen scant feature-files die
9
- // `import { z } from "zod"` haben.
10
-
11
- import { afterAll, describe, expect, test } from "bun:test";
12
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
13
- import { dirname, join } from "node:path";
14
- import type { CodegenResult } from "../run-codegen";
15
- import { watchAndRegenerate } from "../watch";
16
-
17
- const TEST_FIXTURE_DIR = join(__dirname, ".tmp-fixtures");
18
- const createdDirs: string[] = [];
19
-
20
- function makeAppDir(): string {
21
- mkdirSync(TEST_FIXTURE_DIR, { recursive: true });
22
- const dir = mkdtempSync(join(TEST_FIXTURE_DIR, "watch-"));
23
- createdDirs.push(dir);
24
- return dir;
25
- }
26
-
27
- function writeFile(dir: string, relPath: string, content: string): string {
28
- const full = join(dir, relPath);
29
- mkdirSync(dirname(full), { recursive: true });
30
- writeFileSync(full, content, "utf-8");
31
- return full;
32
- }
33
-
34
- afterAll(() => {
35
- for (const d of createdDirs) {
36
- try {
37
- rmSync(d, { recursive: true, force: true });
38
- } catch {
39
- /* best-effort */
40
- }
41
- }
42
- });
43
-
44
- /**
45
- * Polls a predicate at `interval` ms until it returns true, or rejects
46
- * after `timeout`. Replaces fixed `setTimeout(...)` waits — those
47
- * implicitly assume "this many ms is enough", which is brittle on
48
- * loaded CI runners. The polling form converges as fast as the system
49
- * allows AND fails loudly with a useful message if the event never lands.
50
- *
51
- * `retry`/`retryIntervalMs`: for predicates gated on a native `fs.watch`
52
- * event, a single write only gets one chance at delivery — under macOS
53
- * FSEvents backlog (hundreds of concurrent recursive watches in a full
54
- * `bun test` run), events aren't just delayed, they're sometimes dropped
55
- * entirely, and Node doesn't surface a rescan signal. `retry` re-fires the
56
- * triggering action on a cadence so a dropped event costs one interval,
57
- * not the whole timeout. `retryIntervalMs` must stay well above the
58
- * watcher's `debounceMs` — a retry that lands mid-debounce just resets
59
- * the timer and can starve `fire()` forever.
60
- */
61
- async function waitFor(
62
- predicate: () => boolean,
63
- opts: {
64
- timeout?: number;
65
- interval?: number;
66
- label?: string;
67
- retry?: () => void;
68
- retryIntervalMs?: number;
69
- } = {},
70
- ): Promise<void> {
71
- const timeout = opts.timeout ?? 5000;
72
- const interval = opts.interval ?? 25;
73
- const retryIntervalMs = opts.retryIntervalMs ?? 250;
74
- const deadline = Date.now() + timeout;
75
- let nextRetryAt = Date.now() + retryIntervalMs;
76
- // @timeout-exception: #3118 fs.watch can drop the trigger; retry re-fires it, unlike the shared waitFor
77
- while (!predicate()) {
78
- if (Date.now() > deadline) {
79
- throw new Error(`waitFor: ${opts.label ?? "predicate"} not satisfied within ${timeout}ms`);
80
- }
81
- if (opts.retry && Date.now() >= nextRetryAt) {
82
- opts.retry();
83
- nextRetryAt = Date.now() + retryIntervalMs;
84
- }
85
- await new Promise((r) => setTimeout(r, interval));
86
- }
87
- }
88
-
89
- const FEATURE_TEMPLATE = (featureName: string, eventName: string) => `
90
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
91
- import { z } from "zod";
92
-
93
- export default defineFeature("${featureName}", (r) => {
94
- r.defineEvent("${eventName}", z.object({ id: z.string() }));
95
- });
96
- `;
97
-
98
- describe("watchAndRegenerate", () => {
99
- test("initial run produces output synchronously", () => {
100
- const appRoot = makeAppDir();
101
- writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("billing", "first-event"));
102
-
103
- const results: CodegenResult[] = [];
104
- const handle = watchAndRegenerate({
105
- appRoot,
106
- onResult: (r) => results.push(r),
107
- });
108
-
109
- expect(results).toHaveLength(1);
110
- expect(results[0]?.eventCount).toBe(1);
111
- handle.close();
112
- });
113
-
114
- test("file change triggers a re-run after debounce", async () => {
115
- const appRoot = makeAppDir();
116
- writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("orders", "first"));
117
-
118
- const results: CodegenResult[] = [];
119
- const handle = watchAndRegenerate({
120
- appRoot,
121
- debounceMs: 30,
122
- onResult: (r) => results.push(r),
123
- });
124
-
125
- expect(results).toHaveLength(1);
126
- expect(results[0]?.eventCount).toBe(1);
127
-
128
- const rewrite = () =>
129
- writeFile(
130
- appRoot,
131
- "src/feature.ts",
132
- `
133
- import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
134
- import { z } from "zod";
135
-
136
- export default defineFeature("orders", (r) => {
137
- r.defineEvent("first", z.object({ id: z.string() }));
138
- r.defineEvent("second", z.object({ tag: z.string() }));
139
- });
140
- `,
141
- );
142
-
143
- // Add a second event-definition by rewriting the feature.
144
- rewrite();
145
-
146
- // Poll until the watcher's debounced re-run has landed, re-touching
147
- // the file every 250ms in case the triggering fs.watch event was
148
- // dropped rather than merely delayed (see waitFor's `retry` doc).
149
- // Waits on the expected *state* (eventCount 2), not just a length
150
- // bump — writeFileSync truncates then writes, so an event fired
151
- // mid-write would satisfy a length-only predicate with a stale
152
- // (0 or 1) eventCount, especially with retries widening that window.
153
- try {
154
- await waitFor(() => results.some((r) => r.eventCount === 2), {
155
- timeout: 12000,
156
- label: "second codegen result",
157
- retry: rewrite,
158
- });
159
- expect(results.at(-1)?.eventCount).toBe(2);
160
- } finally {
161
- handle.close();
162
- }
163
- }, 15000);
164
-
165
- test("close() is idempotent", () => {
166
- const appRoot = makeAppDir();
167
- writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("nope", "evt"));
168
- const handle = watchAndRegenerate({ appRoot, onResult: () => {} });
169
- handle.close();
170
- expect(() => handle.close()).not.toThrow();
171
- });
172
-
173
- test("non-ts file changes do not trigger codegen", async () => {
174
- // Negative-assertion shape: prove that .css/.md changes do NOT add
175
- // a codegen result. Naïve "sleep N ms then assert length stayed"
176
- // is racy on macOS, where fs.watch can deliver stale events from
177
- // pre-watcher writes after the watcher is attached. We sidestep
178
- // that by anchoring on a POSITIVE control: a known-triggering .ts
179
- // change at the end. waitFor proves the watcher is alive — so the
180
- // pre-trigger count is trustworthy.
181
- const appRoot = makeAppDir();
182
- writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("ignore-css", "evt"));
183
-
184
- const results: CodegenResult[] = [];
185
- const handle = watchAndRegenerate({
186
- appRoot,
187
- debounceMs: 30,
188
- onResult: (r) => results.push(r),
189
- });
190
- expect(results).toHaveLength(1);
191
-
192
- // Drain any stale events from the pre-watcher feature.ts write —
193
- // some platforms deliver these to a watcher attached after the
194
- // write. Long enough to outlast debounce + scheduler jitter.
195
- await new Promise((r) => setTimeout(r, 200));
196
- const baseline = results.length;
197
-
198
- // Non-ts writes — the regression we want to catch.
199
- writeFile(appRoot, "src/styles.css", `body { color: red; }`);
200
- writeFile(appRoot, "src/README.md", `# hi`);
201
- await new Promise((r) => setTimeout(r, 200));
202
- const afterNonTs = results.length;
203
-
204
- // Positive control: a .ts change MUST trigger. waitFor exits as
205
- // soon as the new result lands, confirming the watcher is alive.
206
- // Re-touched on retry in case the triggering event is dropped
207
- // rather than merely delayed under a loaded fs.watch backlog.
208
- const triggerRewrite = () =>
209
- writeFile(appRoot, "src/feature.ts", FEATURE_TEMPLATE("ignore-css", "after"));
210
- triggerRewrite();
211
- await waitFor(() => results.length > afterNonTs, {
212
- timeout: 12000,
213
- label: "ts-change result after non-ts noise",
214
- retry: triggerRewrite,
215
- });
216
-
217
- // The non-ts writes should not have advanced the count past the
218
- // baseline. If they did, the watcher's filter is broken.
219
- expect(afterNonTs).toBe(baseline);
220
- handle.close();
221
- }, 15000);
222
- });
@@ -1,19 +0,0 @@
1
- export {
2
- renderDefineFile,
3
- renderInlineSchemasFile,
4
- renderTypesAugmentation,
5
- renderWriteHandlerTypes,
6
- } from "./render";
7
- export { type CodegenOptions, type CodegenResult, runCodegen } from "./run-codegen";
8
- export {
9
- formatScanWarning,
10
- qualifiedNameToConstName,
11
- rewriteImportPath,
12
- type ScannedEvent,
13
- type ScanOptions,
14
- type ScanResult,
15
- type ScanWarning,
16
- type SchemaSource,
17
- scanEvents,
18
- } from "./scan-events";
19
- export { type WatchHandle, type WatchOptions, watchAndRegenerate } from "./watch";