@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.2

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,565 +0,0 @@
1
- // scanEvents — finds every `r.defineEvent(...)` call across the app's
2
- // feature files and resolves both the event-name (string literal) AND
3
- // the schema source needed to derive a TS-Type. The codegen pipeline
4
- // turns these into a `KumikoEventTypeMap` augmentation, the "single
5
- // source of truth" the local defineWriteHandler-wrapper binds against
6
- // for cross-package strict-checking.
7
- //
8
- // Output is a flat list of "event entries" — qualified-name + everything
9
- // needed to emit a `z.infer<typeof Schema>` line in the augmentation.
10
- //
11
- // Patterns we resolve to a strict-checked type:
12
- // 1. r.defineEvent("name", schemaIdentifier)
13
- // Position-form, named import. The cleanest case — Schema lives
14
- // in events.ts, gets re-imported by the augmentation as `import
15
- // type` and threaded through `z.infer<typeof Schema>`.
16
- //
17
- // 2. r.defineEvent({ name, schema })
18
- // Object-form, otherwise identical to (1).
19
- //
20
- // 3. r.defineEvent(NAME_CONST.member, schema...)
21
- // Computed name via `as const` object. We follow the property-
22
- // access through ts-morph's symbol-resolver, find the literal
23
- // member, treat it as the string from (1)/(2). Keeps recipes that
24
- // centralise event-names in a constants module strict-able.
25
- //
26
- // 4. r.defineEvent(name..., z.object({ ... }))
27
- // Inline schema (call-expression instead of an identifier). We
28
- // extract the call-source-text and emit it into a co-generated
29
- // `schemas.generated.ts` as a named `export const`, then point
30
- // the augmentation at that named export. The original feature
31
- // file keeps its inline schema for runtime validation; the
32
- // generated schemas-file exists ONLY for type-inference (consumed
33
- // via `import type`, erased at build).
34
- //
35
- // What we still skip with a warning:
36
- // - Default- or namespace-imports of the schema identifier
37
- // (`import S from "..."`, `import * as M from "..."`). Rare in
38
- // real apps; signal-to-noise of supporting them isn't worth it.
39
- // - Computed names whose const cannot be statically resolved
40
- // (cross-package re-exports, dynamic property access).
41
-
42
- import { readdirSync, statSync } from "node:fs";
43
- import { join, relative, resolve, sep } from "node:path";
44
- import { toKebab } from "@cosmicdrift/kumiko-framework/engine";
45
- import {
46
- type CallExpression,
47
- type ImportDeclaration,
48
- type Node,
49
- Project,
50
- type PropertyAccessExpression,
51
- type SourceFile,
52
- SyntaxKind,
53
- } from "ts-morph";
54
-
55
- export type ScannedEvent = {
56
- /** Qualified event-name as it appears in the events table:
57
- * `<feature>:event:<inner>`. The KumikoEventTypeMap key. */
58
- readonly qualifiedName: string;
59
- /** Where the type for this event's payload comes from. Two flavours:
60
- *
61
- * { kind: "imported", schemaIdentifier, schemaModulePath }
62
- * Schema is a named export of another file. Augmentation
63
- * `import type`-s it from there.
64
- *
65
- * { kind: "inline", schemaSource, generatedConstName }
66
- * Schema was inlined at the call-site. We extract the source
67
- * text into `schemas.generated.ts` under `generatedConstName`
68
- * and import-type from there. */
69
- readonly schemaSource: SchemaSource;
70
- /** Absolute disk-path of the feature file (for relative-path
71
- * resolution + diagnostics). */
72
- readonly featureFilePath: string;
73
- /** For diagnostics + dedup logging. */
74
- readonly source: { readonly file: string; readonly line: number };
75
- };
76
-
77
- export type SchemaSource =
78
- | {
79
- readonly kind: "imported";
80
- readonly schemaIdentifier: string;
81
- readonly schemaModulePath: string;
82
- }
83
- | {
84
- readonly kind: "inline";
85
- /** zod source text — `z.object({ id: z.string() })` etc. */
86
- readonly schemaSource: string;
87
- /** Name we'll generate inside `schemas.generated.ts`. Stable
88
- * + qualified-name-derived so reorder of features doesn't
89
- * rename them. */
90
- readonly generatedConstName: string;
91
- };
92
-
93
- export type ScanWarning = {
94
- /** Absent for warnings that aren't tied to a scanned source line
95
- * (e.g. a codegen-level warning about a missing manifest file). */
96
- readonly file?: string;
97
- readonly line?: number;
98
- readonly message: string;
99
- };
100
-
101
- export type ScanResult = {
102
- readonly events: readonly ScannedEvent[];
103
- readonly warnings: readonly ScanWarning[];
104
- };
105
-
106
- /** Renders a `ScanWarning` for terminal output; omits the location prefix when the warning has no source line. */
107
- export function formatScanWarning(warning: ScanWarning): string {
108
- return warning.file !== undefined
109
- ? `${warning.file}:${warning.line} — ${warning.message}`
110
- : warning.message;
111
- }
112
-
113
- export type ScanOptions = {
114
- /** App-Wurzel — alles unter `<root>/src` wird gescannt. Tests +
115
- * generated-files (`.kumiko`, `dist*`, `node_modules`) sind raus. */
116
- readonly appRoot: string;
117
- };
118
-
119
- /**
120
- * Scant `appRoot/src/**` nach `r.defineEvent(...)`-Aufrufen und liefert
121
- * eine deduplizierte Liste aus ScannedEvent. Doppelte qualifiedName
122
- * landen mit einer Warnung in der Result — Codegen schreibt nur den
123
- * ERSTEN, sodass das generated File compile-stabil bleibt.
124
- */
125
- export function scanEvents(opts: ScanOptions): ScanResult {
126
- const project = new Project({
127
- skipAddingFilesFromTsConfig: true,
128
- skipFileDependencyResolution: true,
129
- });
130
-
131
- const filesToScan: string[] = [];
132
- collectTsFiles(join(opts.appRoot, "src"), filesToScan);
133
-
134
- // Add ALL files to the project up-front. ts-morph's symbol-resolver
135
- // needs to see the file that declares the const-object before it can
136
- // follow `INVOICE_EVENTS.sent` to its string literal.
137
- for (const filePath of filesToScan) {
138
- project.addSourceFileAtPath(filePath);
139
- }
140
-
141
- const events: ScannedEvent[] = [];
142
- const warnings: ScanWarning[] = [];
143
-
144
- for (const filePath of filesToScan) {
145
- const sourceFile = project.getSourceFile(filePath);
146
- if (!sourceFile) continue;
147
- scanFile(sourceFile, filePath, events, warnings);
148
- }
149
-
150
- return { events: dedupe(events, warnings), warnings };
151
- }
152
-
153
- // ============================================================================
154
- // Internal — directory walk
155
- // ============================================================================
156
-
157
- const SKIP_SEGMENTS = new Set(["node_modules", ".kumiko", "dist", "dist-server", "__tests__"]);
158
-
159
- function collectTsFiles(dir: string, out: string[]): void {
160
- let entries: string[];
161
- try {
162
- entries = readdirSync(dir);
163
- } catch {
164
- // skip: directory unreadable, nothing to scan there
165
- return;
166
- }
167
- for (const entry of entries) {
168
- if (entry.startsWith(".")) continue;
169
- if (SKIP_SEGMENTS.has(entry)) continue;
170
- const full = join(dir, entry);
171
- let stat: ReturnType<typeof statSync>;
172
- try {
173
- stat = statSync(full);
174
- } catch {
175
- continue;
176
- }
177
- if (stat.isDirectory()) {
178
- collectTsFiles(full, out);
179
- } else if (
180
- stat.isFile() &&
181
- (entry.endsWith(".ts") || entry.endsWith(".tsx")) &&
182
- !entry.endsWith(".d.ts") &&
183
- !entry.endsWith(".test.ts") &&
184
- !entry.endsWith(".test.tsx")
185
- ) {
186
- out.push(full);
187
- }
188
- }
189
- }
190
-
191
- // ============================================================================
192
- // Internal — per-file scan
193
- // ============================================================================
194
-
195
- function scanFile(
196
- sourceFile: SourceFile,
197
- filePath: string,
198
- events: ScannedEvent[],
199
- warnings: ScanWarning[],
200
- ): void {
201
- // Find every defineFeature(...) — there may be multiple in factory-style
202
- // packages (one factory function per feature). Each carries its own
203
- // featureName (1st arg) + setup-arrow-function (2nd arg).
204
- for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
205
- if (call.getExpression().getText() !== "defineFeature") continue;
206
- const featureName = readStringLiteral(call.getArguments()[0]);
207
- if (!featureName) continue;
208
- const setup = call.getArguments()[1]?.asKind(SyntaxKind.ArrowFunction);
209
- if (!setup) continue;
210
- const registrarParam = setup.getParameters()[0]?.getName();
211
- if (!registrarParam) continue;
212
- for (const defCall of setup.getBody().getDescendantsOfKind(SyntaxKind.CallExpression)) {
213
- const expr = defCall.getExpression().asKind(SyntaxKind.PropertyAccessExpression);
214
- if (!expr) continue;
215
- if (expr.getExpression().getText() !== registrarParam) continue;
216
- if (expr.getName() !== "defineEvent") continue;
217
-
218
- collectFromDefineEvent(defCall, sourceFile, featureName, filePath, events, warnings);
219
- }
220
- }
221
- }
222
-
223
- function collectFromDefineEvent(
224
- call: CallExpression,
225
- sourceFile: SourceFile,
226
- featureName: string,
227
- filePath: string,
228
- events: ScannedEvent[],
229
- warnings: ScanWarning[],
230
- ): void {
231
- const parsed = parseDefineEventCall(call);
232
- if (!parsed) {
233
- warnings.push({
234
- file: filePath,
235
- line: call.getStartLineNumber(),
236
- message: "r.defineEvent: cannot read event-name + schema statically",
237
- });
238
- // skip: defineEvent call shape not statically parseable, already recorded as warning
239
- return;
240
- }
241
-
242
- // Mirror the framework's `qn(toKebab(feature), "event", toKebab(name))`
243
- // — the augmentation key MUST match what `r.defineEvent` writes at
244
- // runtime. Without the kebab-step, `defineFeature("driverOrders")`
245
- // would augment `driverOrders:event:*` while the runtime stream
246
- // carries `driver-orders:event:*`, and strict-mode would reject every
247
- // correct call.
248
- const qualifiedName = `${toKebab(featureName)}:event:${toKebab(parsed.eventName)}`;
249
- const schema = resolveSchemaSource(parsed.schemaNode, sourceFile, qualifiedName);
250
- if (!schema) {
251
- warnings.push({
252
- file: filePath,
253
- line: call.getStartLineNumber(),
254
- message: `r.defineEvent("${parsed.eventName}"): schema "${parsed.schemaNode.getText()}" — not a named import nor an inline z.* call, skipped`,
255
- });
256
- // skip: schema source not resolvable, already recorded as warning
257
- return;
258
- }
259
-
260
- events.push({
261
- qualifiedName,
262
- schemaSource: schema,
263
- featureFilePath: filePath,
264
- source: { file: filePath, line: call.getStartLineNumber() },
265
- });
266
- }
267
-
268
- // ============================================================================
269
- // Internal — extract event-name + schema-node
270
- // ============================================================================
271
-
272
- type ParsedDefineEvent = {
273
- /** Final `<inner>` part of the qualified name. Always a string-literal
274
- * AFTER resolution (we follow PropertyAccess → const-member). */
275
- readonly eventName: string;
276
- /** AST node for the schema argument — left to be resolved by
277
- * resolveSchemaSource into either an imported-named or inline-call. */
278
- readonly schemaNode: Node;
279
- };
280
-
281
- function parseDefineEventCall(call: CallExpression): ParsedDefineEvent | undefined {
282
- const args = call.getArguments();
283
- const first = args[0];
284
- if (!first) return undefined;
285
-
286
- // Object-form: r.defineEvent({ name, schema, version? })
287
- const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
288
- if (obj && args.length === 1) {
289
- const nameProp = obj
290
- .getProperty("name")
291
- ?.asKind(SyntaxKind.PropertyAssignment)
292
- ?.getInitializer();
293
- const eventName = nameProp ? resolveStringLiteralOrConst(nameProp) : undefined;
294
- if (!eventName) return undefined;
295
- const schemaNode = obj
296
- .getProperty("schema")
297
- ?.asKind(SyntaxKind.PropertyAssignment)
298
- ?.getInitializer();
299
- if (!schemaNode) return undefined;
300
- return { eventName, schemaNode };
301
- }
302
-
303
- // Position-form: r.defineEvent(<name-source>, <schema-node>, ...)
304
- const eventName = resolveStringLiteralOrConst(first);
305
- const schemaNode = args[1];
306
- if (!eventName || !schemaNode) return undefined;
307
- return { eventName, schemaNode };
308
- }
309
-
310
- /**
311
- * Resolves a node to a string literal — either it IS a string-literal,
312
- * or it's a property-access on a `const X = { ... } as const` object
313
- * whose member resolves to one. Returns undefined for anything else
314
- * (template literals, dynamic property access, function calls).
315
- *
316
- * Cross-file resolution: we follow the named import that brings the
317
- * receiver-identifier into scope, load the target file from the same
318
- * ts-morph project, and look up the const-declaration there. This
319
- * works WITHOUT a TypeChecker — the import-graph + AST is enough for
320
- * the patterns the recipes/showcases use (`as const` objects with
321
- * string-literal members).
322
- */
323
- function resolveStringLiteralOrConst(node: Node): string | undefined {
324
- // Direct string-literal — fast path.
325
- const direct = node.asKind(SyntaxKind.StringLiteral);
326
- if (direct) return direct.getLiteralValue();
327
-
328
- // PropertyAccessExpression: `INVOICE_EVENTS.sent` form.
329
- const propAccess = node.asKind(SyntaxKind.PropertyAccessExpression);
330
- if (propAccess) return resolvePropertyAccessLiteral(propAccess);
331
-
332
- return undefined;
333
- }
334
-
335
- function resolvePropertyAccessLiteral(propAccess: PropertyAccessExpression): string | undefined {
336
- const receiver = propAccess.getExpression().asKind(SyntaxKind.Identifier);
337
- if (!receiver) return undefined;
338
- const memberName = propAccess.getName();
339
- const callerFile = propAccess.getSourceFile();
340
-
341
- // Two paths to find the const-declaration:
342
- // 1. Local: declared in the same file before the call.
343
- // 2. Imported: a named import points at another file in the project;
344
- // the const lives there as an `export const`.
345
- const local = findConstObject(callerFile, receiver.getText());
346
- if (local) return readMemberLiteral(local, memberName);
347
-
348
- for (const importDecl of callerFile.getImportDeclarations()) {
349
- if (!matchesNamedImport(importDecl, receiver.getText())) continue;
350
- const targetFile = resolveImportedSourceFile(importDecl, callerFile);
351
- if (!targetFile) continue;
352
- const remote = findConstObject(targetFile, receiver.getText());
353
- if (remote) {
354
- const literal = readMemberLiteral(remote, memberName);
355
- if (literal !== undefined) return literal;
356
- }
357
- }
358
- return undefined;
359
- }
360
-
361
- /**
362
- * Walk top-level statements for `const RECEIVER = { ... } [as const]`.
363
- * Returns the inner object-literal if found.
364
- */
365
- function findConstObject(sourceFile: SourceFile, receiverName: string): Node | undefined {
366
- for (const stmt of sourceFile.getVariableStatements()) {
367
- for (const decl of stmt.getDeclarations()) {
368
- if (decl.getName() !== receiverName) continue;
369
- const init = unwrapAsConst(decl.getInitializer());
370
- const objLit = init?.asKind(SyntaxKind.ObjectLiteralExpression);
371
- if (objLit) return objLit;
372
- }
373
- }
374
- return undefined;
375
- }
376
-
377
- function readMemberLiteral(objLitNode: Node, memberName: string): string | undefined {
378
- const objLit = objLitNode.asKind(SyntaxKind.ObjectLiteralExpression);
379
- if (!objLit) return undefined;
380
- const prop = objLit.getProperty(memberName)?.asKind(SyntaxKind.PropertyAssignment);
381
- const initLit = prop?.getInitializer()?.asKind(SyntaxKind.StringLiteral);
382
- return initLit?.getLiteralValue();
383
- }
384
-
385
- /**
386
- * Resolve a relative import-specifier to a SourceFile already loaded
387
- * in the project. Tries `<base>.ts` and `<base>/index.ts`. Returns
388
- * undefined for npm-package specifiers (no local resolution needed —
389
- * we don't follow anything beyond the app's own files).
390
- */
391
- function resolveImportedSourceFile(
392
- importDecl: ImportDeclaration,
393
- fromFile: SourceFile,
394
- ): SourceFile | undefined {
395
- const spec = importDecl.getModuleSpecifierValue();
396
- if (!spec.startsWith(".")) return undefined;
397
- const fromDir = fromFile.getDirectoryPath();
398
- const project = fromFile.getProject();
399
- const candidates = [`${spec}.ts`, `${spec}.tsx`, `${spec}/index.ts`, `${spec}/index.tsx`];
400
- for (const cand of candidates) {
401
- const abs = resolve(fromDir, cand);
402
- const sf = project.getSourceFile(abs);
403
- if (sf) return sf;
404
- }
405
- return undefined;
406
- }
407
-
408
- /**
409
- * `{ x: 1 } as const` parses as `AsExpression > ObjectLiteralExpression`.
410
- * Strip the AsExpression so the caller can hit the inner literal directly.
411
- */
412
- function unwrapAsConst(node: Node | undefined): Node | undefined {
413
- if (!node) return undefined;
414
- const asExpr = node.asKind(SyntaxKind.AsExpression);
415
- return asExpr ? asExpr.getExpression() : node;
416
- }
417
-
418
- function readStringLiteral(node: Node | undefined): string | undefined {
419
- return node?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
420
- }
421
-
422
- // ============================================================================
423
- // Internal — resolve schema node to its source kind
424
- // ============================================================================
425
-
426
- function resolveSchemaSource(
427
- schemaNode: Node,
428
- sourceFile: SourceFile,
429
- qualifiedName: string,
430
- ): SchemaSource | undefined {
431
- // Named identifier: schema lives in another file as a named export.
432
- const ident = schemaNode.asKind(SyntaxKind.Identifier);
433
- if (ident) {
434
- const importInfo = resolveSchemaImport(sourceFile, ident.getText());
435
- if (!importInfo) return undefined;
436
- return {
437
- kind: "imported",
438
- schemaIdentifier: ident.getText(),
439
- schemaModulePath: importInfo.moduleSpecifier,
440
- };
441
- }
442
-
443
- // Inline call: r.defineEvent("name", z.object({...})).
444
- // We accept any call-expression that LOOKS like a zod schema (cheap
445
- // structural check on the `z.*` head). The exact ".object/.string/etc"
446
- // doesn't matter — the codegen will replay the source text in the
447
- // schemas-file, where TS resolves z.infer correctly.
448
- const callExpr = schemaNode.asKind(SyntaxKind.CallExpression);
449
- if (callExpr && looksLikeZodCall(callExpr)) {
450
- return {
451
- kind: "inline",
452
- schemaSource: callExpr.getText(),
453
- generatedConstName: qualifiedNameToConstName(qualifiedName),
454
- };
455
- }
456
-
457
- return undefined;
458
- }
459
-
460
- function looksLikeZodCall(call: CallExpression): boolean {
461
- // Walk the callee head — `z.something(...)` or `z.something.foo(...)`,
462
- // anything that traces back to an Identifier `z`. Conservative: we
463
- // don't try to verify it's the actual zod-import (the runtime check
464
- // happens through the schemas-file's `import { z } from "zod"` anyway,
465
- // which fails loudly if the user's `z` is something else).
466
- let cur: Node = call.getExpression();
467
- while (cur.asKind(SyntaxKind.PropertyAccessExpression) || cur.asKind(SyntaxKind.CallExpression)) {
468
- const prop = cur.asKind(SyntaxKind.PropertyAccessExpression);
469
- if (prop) {
470
- cur = prop.getExpression();
471
- continue;
472
- }
473
- const innerCall = cur.asKind(SyntaxKind.CallExpression);
474
- if (innerCall) {
475
- cur = innerCall.getExpression();
476
- }
477
- }
478
- return cur.asKind(SyntaxKind.Identifier)?.getText() === "z";
479
- }
480
-
481
- function resolveSchemaImport(
482
- sourceFile: SourceFile,
483
- identifier: string,
484
- ): { readonly moduleSpecifier: string } | undefined {
485
- for (const importDecl of sourceFile.getImportDeclarations()) {
486
- if (matchesNamedImport(importDecl, identifier)) {
487
- return { moduleSpecifier: importDecl.getModuleSpecifierValue() };
488
- }
489
- }
490
- return undefined;
491
- }
492
-
493
- function matchesNamedImport(importDecl: ImportDeclaration, identifier: string): boolean {
494
- for (const named of importDecl.getNamedImports()) {
495
- // Handles both `import { x }` and `import { x as y }` — we want the
496
- // LOCAL alias (what's used in the call site), which matches the
497
- // identifier the scanner extracted.
498
- const localName = named.getAliasNode()?.getText() ?? named.getNameNode().getText();
499
- if (localName === identifier) return true;
500
- }
501
- return false;
502
- }
503
-
504
- /**
505
- * Stable identifier-safe rewrite of a qualifiedName like
506
- * `pubsubOrders:event:order-placed` → `_kg_pubsubOrders__orderPlaced`.
507
- * Used for the `export const` name in `schemas.generated.ts`. Same
508
- * transform in scan + render keeps the two sides in sync.
509
- */
510
- export function qualifiedNameToConstName(qualifiedName: string): string {
511
- // Drop the ":event:" infix — every entry has it, no information.
512
- const withoutEventInfix = qualifiedName.replace(/:event:/g, "__");
513
- // Replace remaining colons + dashes with `_` and camel-case after `_`
514
- // so the output is identifier-legal AND visually parseable.
515
- const sanitised = withoutEventInfix.replace(/[^A-Za-z0-9_]+(.?)/g, (_match, next: string) =>
516
- next.toUpperCase(),
517
- );
518
- return `_kg_${sanitised}`;
519
- }
520
-
521
- // ============================================================================
522
- // Internal — module-specifier rewriting + dedup
523
- // ============================================================================
524
-
525
- /**
526
- * Resolves the relative schema-import path from the feature-file's
527
- * point-of-view to a path relative to `.kumiko/` (which is where the
528
- * generated d.ts file lives). Workspace-package specifiers
529
- * (`@cosmicdrift/kumiko-...`) are returned as-is.
530
- */
531
- export function rewriteImportPath(
532
- schemaModulePath: string,
533
- featureFilePath: string,
534
- outputDirAbs: string,
535
- ): string {
536
- // Workspace / npm specifiers — pass through. Codegen output imports
537
- // them by name.
538
- if (!schemaModulePath.startsWith(".")) return schemaModulePath;
539
-
540
- const featureDir = featureFilePath.substring(0, featureFilePath.lastIndexOf(sep));
541
- const absoluteSchemaPath = resolve(featureDir, schemaModulePath);
542
- const fromOutput = relative(outputDirAbs, absoluteSchemaPath);
543
- // POSIX-Slash für TS-imports (auch auf Windows).
544
- const normalised = fromOutput.split(sep).join("/");
545
- // Strip .ts/.tsx — TS module specifiers don't carry the extension;
546
- // resolve() didn't add one but a hand-written ".ts" should be removed.
547
- return normalised.replace(/\.tsx?$/, "");
548
- }
549
-
550
- function dedupe(events: ScannedEvent[], warnings: ScanWarning[]): ScannedEvent[] {
551
- const seen = new Map<string, ScannedEvent>();
552
- for (const ev of events) {
553
- const existing = seen.get(ev.qualifiedName);
554
- if (existing) {
555
- warnings.push({
556
- file: ev.source.file,
557
- line: ev.source.line,
558
- message: `duplicate r.defineEvent("${ev.qualifiedName}") — first declared at ${existing.source.file}:${existing.source.line}, ignored here`,
559
- });
560
- continue;
561
- }
562
- seen.set(ev.qualifiedName, ev);
563
- }
564
- return [...seen.values()];
565
- }