@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.
- package/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- package/src/welcome-banner.ts +0 -43
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// Renders the three generated files under `<appRoot>/.kumiko/`:
|
|
2
|
+
//
|
|
3
|
+
// 1. `types.generated.d.ts` — augments `KumikoEventTypeMap` with every
|
|
4
|
+
// `r.defineEvent` entry. Pure declarations, no runtime code.
|
|
5
|
+
//
|
|
6
|
+
// 2. `schemas.generated.ts` — re-exports of inline zod schemas, one
|
|
7
|
+
// per `r.defineEvent("name", z.object({...}))` call that wasn't
|
|
8
|
+
// already an imported named export. This file is referenced
|
|
9
|
+
// EXCLUSIVELY via `import type` (in types.generated.d.ts) — the
|
|
10
|
+
// ts-typescript-strip pass elides it at build time, no runtime
|
|
11
|
+
// duplication. Only emitted when at least one inline schema exists.
|
|
12
|
+
//
|
|
13
|
+
// 3. `define.ts` — the local `defineWriteHandler` /
|
|
14
|
+
// `defineQueryHandler` wrappers that pin TMap explicitly. THIS is
|
|
15
|
+
// where strict mode actually takes effect (see project_x1_typemap_
|
|
16
|
+
// findings memory): cross-package generic functions with the
|
|
17
|
+
// default TMap do NOT see the augmentation — only a local wrapper
|
|
18
|
+
// with an explicit TMap gets it.
|
|
19
|
+
//
|
|
20
|
+
// All three files are written idempotently — same inputs ⇒ same string.
|
|
21
|
+
// The codegen run compares new vs existing content and only writes on
|
|
22
|
+
// actual change, so mtime doesn't tick and the TS language server
|
|
23
|
+
// doesn't reload every 100ms.
|
|
24
|
+
import { basename, relative } from "node:path";
|
|
25
|
+
import { rewriteImportPath } from "./scan-events.js";
|
|
26
|
+
/**
|
|
27
|
+
* Start-of-block markers for the two handler-derived sections that
|
|
28
|
+
* `run-codegen.ts` may need to carry forward verbatim from a previous
|
|
29
|
+
* run (when the current run has no `handlerQns` to render a fresh
|
|
30
|
+
* block from). Each marker is always the last thing its renderer
|
|
31
|
+
* emits, so a caller can locate it in a previously-written file and
|
|
32
|
+
* slice from the marker to EOF to recover the whole block.
|
|
33
|
+
*/
|
|
34
|
+
export const WRITE_HANDLER_QN_MARKER = "// --- WriteHandlerQn (generated from live registry / feature-manifest.json) ---";
|
|
35
|
+
export const TYPED_DISPATCHER_MARKER = "// --- TypedDispatcher (generated from live registry / feature-manifest.json) ---";
|
|
36
|
+
const HEADER = [
|
|
37
|
+
"// =====================================================================",
|
|
38
|
+
"// AUTO-GENERATED — DO NOT EDIT BY HAND",
|
|
39
|
+
"// Regenerated by `bun run build` (kumiko-build) or by the dev-server's",
|
|
40
|
+
"// file-watcher, which runs codegen on every r.defineEvent change.",
|
|
41
|
+
"// =====================================================================",
|
|
42
|
+
].join("\n");
|
|
43
|
+
/**
|
|
44
|
+
* Render `types.generated.d.ts`. Imports the schema-identifiers as
|
|
45
|
+
* type-only — sources can be either named exports of feature-events
|
|
46
|
+
* files (kind: "imported") or extracted const-exports of the
|
|
47
|
+
* co-generated schemas.generated.ts (kind: "inline"). Result is wired
|
|
48
|
+
* via `z.infer<typeof X>` into the augmentation. Empty events list →
|
|
49
|
+
* minimal but valid file (still augmentable, just no entries yet —
|
|
50
|
+
* useful first-time scaffold).
|
|
51
|
+
*/
|
|
52
|
+
export function renderTypesAugmentation(events, outputDirAbs) {
|
|
53
|
+
// Group identifiers by their resolved (rewritten) module path so each
|
|
54
|
+
// schema file imports once. Multiple events sharing the same events.ts
|
|
55
|
+
// file is the common case for the "imported" kind. Inline-events all
|
|
56
|
+
// share the same module path: "./schemas.generated".
|
|
57
|
+
const importsByPath = new Map();
|
|
58
|
+
for (const ev of events) {
|
|
59
|
+
if (ev.schemaSource.kind === "imported") {
|
|
60
|
+
const rewritten = rewriteImportPath(ev.schemaSource.schemaModulePath, ev.featureFilePath, outputDirAbs);
|
|
61
|
+
if (!importsByPath.has(rewritten))
|
|
62
|
+
importsByPath.set(rewritten, new Set());
|
|
63
|
+
importsByPath.get(rewritten)?.add(ev.schemaSource.schemaIdentifier);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const path = "./schemas.generated";
|
|
67
|
+
if (!importsByPath.has(path))
|
|
68
|
+
importsByPath.set(path, new Set());
|
|
69
|
+
importsByPath.get(path)?.add(ev.schemaSource.generatedConstName);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const importLines = [];
|
|
73
|
+
// `z` from zod is needed for `z.infer<typeof X>`; we import it once.
|
|
74
|
+
importLines.push(`import type { z } from "zod";`);
|
|
75
|
+
// Stable order — sort module paths alphabetically; identifiers within
|
|
76
|
+
// a module also alphabetically. Idempotent output.
|
|
77
|
+
for (const [modPath, idents] of [...importsByPath.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
78
|
+
const sortedIdents = [...idents].sort();
|
|
79
|
+
importLines.push(`import type { ${sortedIdents.join(", ")} } from "${modPath}";`);
|
|
80
|
+
}
|
|
81
|
+
const mapEntries = [...events]
|
|
82
|
+
.sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName))
|
|
83
|
+
.map((ev) => {
|
|
84
|
+
const refName = ev.schemaSource.kind === "imported"
|
|
85
|
+
? ev.schemaSource.schemaIdentifier
|
|
86
|
+
: ev.schemaSource.generatedConstName;
|
|
87
|
+
return ` "${ev.qualifiedName}": z.infer<typeof ${refName}>;`;
|
|
88
|
+
});
|
|
89
|
+
// Body block — `declare module` in module-form (because we have an
|
|
90
|
+
// `export {}` at the end). Module-form makes the augmentation merge
|
|
91
|
+
// into the target interface; a script-form (`export {}` removed)
|
|
92
|
+
// would REPLACE the module's exports.
|
|
93
|
+
const body = [
|
|
94
|
+
'declare module "@cosmicdrift/kumiko-framework/engine" {',
|
|
95
|
+
" interface KumikoEventTypeMap {",
|
|
96
|
+
...(mapEntries.length === 0 ? [" // (no r.defineEvent calls discovered yet)"] : mapEntries),
|
|
97
|
+
" }",
|
|
98
|
+
"}",
|
|
99
|
+
"",
|
|
100
|
+
"export {};",
|
|
101
|
+
"",
|
|
102
|
+
].join("\n");
|
|
103
|
+
return [HEADER, "", ...importLines, "", body].join("\n");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Render `schemas.generated.ts`. One `export const` per inline-schema
|
|
107
|
+
* event, named via the qualifiedName-derived stable identifier. The
|
|
108
|
+
* source-text of the original `z.*(...)` expression is replayed — we
|
|
109
|
+
* don't try to be cleverer than the zod compiler about reconstructing
|
|
110
|
+
* the schema. Returns undefined when no inline-schemas exist (so the
|
|
111
|
+
* runner can skip writing the file entirely).
|
|
112
|
+
*/
|
|
113
|
+
export function renderInlineSchemasFile(events, appRootAbs) {
|
|
114
|
+
const inlines = events.filter((ev) => ev.schemaSource.kind === "inline");
|
|
115
|
+
if (inlines.length === 0)
|
|
116
|
+
return undefined;
|
|
117
|
+
const lines = [
|
|
118
|
+
HEADER,
|
|
119
|
+
"",
|
|
120
|
+
"// Schema extracts purely for type inference: this file is referenced",
|
|
121
|
+
"// from types.generated.d.ts via `import type`. ts-strip elides it at",
|
|
122
|
+
"// build time, so there is NO runtime duplication of the inline schemas",
|
|
123
|
+
"// in feature files. When an event schema changes: rebuild (`bun run",
|
|
124
|
+
"// build`) or let the dev-server regenerate it — otherwise the z.infer",
|
|
125
|
+
"// type drifts from the runtime schema.",
|
|
126
|
+
"",
|
|
127
|
+
`import { z } from "zod";`,
|
|
128
|
+
"",
|
|
129
|
+
];
|
|
130
|
+
// Sort by const-name for stable output.
|
|
131
|
+
const sorted = [...inlines].sort((a, b) => {
|
|
132
|
+
const aName = a.schemaSource.kind === "inline" ? a.schemaSource.generatedConstName : "";
|
|
133
|
+
const bName = b.schemaSource.kind === "inline" ? b.schemaSource.generatedConstName : "";
|
|
134
|
+
return aName.localeCompare(bName);
|
|
135
|
+
});
|
|
136
|
+
for (const ev of sorted) {
|
|
137
|
+
if (ev.schemaSource.kind !== "inline")
|
|
138
|
+
continue;
|
|
139
|
+
const rel = relative(appRootAbs, ev.featureFilePath).split("\\").join("/");
|
|
140
|
+
// Feature files outside the app root (e.g. a bundled dependency) yield a
|
|
141
|
+
// `../../..`-walk that clutters the source comment; fall back to the bare
|
|
142
|
+
// filename. Comment-only — the generated schema is unaffected.
|
|
143
|
+
const sourcePath = rel.startsWith("..") ? basename(ev.featureFilePath) : rel;
|
|
144
|
+
lines.push(`// ${ev.qualifiedName} — from ${sourcePath}:${ev.source.line}`, `export const ${ev.schemaSource.generatedConstName} = ${ev.schemaSource.schemaSource};`, "");
|
|
145
|
+
}
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Render `define.ts` — local thin wrappers that fix TMap to
|
|
150
|
+
* KumikoEventTypeMap. Apps import `defineWriteHandler` /
|
|
151
|
+
* `defineQueryHandler` from this file; framework's strict-overload
|
|
152
|
+
* becomes the only matching overload because TMap is no longer the
|
|
153
|
+
* eager-resolved default.
|
|
154
|
+
*
|
|
155
|
+
* The wrappers are intentionally *thin* — same signature as the
|
|
156
|
+
* framework's, just with TMap pre-bound. Apps stay portable: switch
|
|
157
|
+
* the import back to "@cosmicdrift/kumiko-framework/engine" and you're back on the
|
|
158
|
+
* loose default. Migration is reversible, no behavioural surface change.
|
|
159
|
+
*/
|
|
160
|
+
export function renderDefineFile(handlerQns = []) {
|
|
161
|
+
const typedDispatcherBlock = handlerQns.length > 0
|
|
162
|
+
? [
|
|
163
|
+
"",
|
|
164
|
+
TYPED_DISPATCHER_MARKER,
|
|
165
|
+
`import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`,
|
|
166
|
+
`import type { WriteHandlerQn } from "./types.generated";`,
|
|
167
|
+
"",
|
|
168
|
+
// Plain `export type { WriteHandlerQn } from "./types.generated"`
|
|
169
|
+
// only re-exports — with verbatimModuleSyntax it does NOT bind the
|
|
170
|
+
// name locally, so the `TypedDispatcher` reference below would fail
|
|
171
|
+
// to resolve. Import above + bare re-export here does both.
|
|
172
|
+
`export type { WriteHandlerQn };`,
|
|
173
|
+
"",
|
|
174
|
+
`/** Dispatcher with write() narrowed to registered handler QNs. */`,
|
|
175
|
+
`export type TypedDispatcher = Omit<Dispatcher, "write"> & {`,
|
|
176
|
+
` write<TData = unknown>(`,
|
|
177
|
+
` type: WriteHandlerQn,`,
|
|
178
|
+
` payload: unknown,`,
|
|
179
|
+
` opts?: WriteOpts,`,
|
|
180
|
+
` ): Promise<WriteResult<TData>>;`,
|
|
181
|
+
`};`,
|
|
182
|
+
"",
|
|
183
|
+
`/** Wrap any Dispatcher so write() only accepts registered QNs at compile time. */`,
|
|
184
|
+
`export function createTypedDispatcher(dispatcher: Dispatcher): TypedDispatcher {`,
|
|
185
|
+
` return {`,
|
|
186
|
+
` ...dispatcher,`,
|
|
187
|
+
` write: (type, payload, opts) => dispatcher.write(type, payload, opts),`,
|
|
188
|
+
` };`,
|
|
189
|
+
`}`,
|
|
190
|
+
"",
|
|
191
|
+
].join("\n")
|
|
192
|
+
: "";
|
|
193
|
+
const body = [
|
|
194
|
+
HEADER,
|
|
195
|
+
"",
|
|
196
|
+
"// Triple-slash reference pulls the augmentation into this compile-",
|
|
197
|
+
"// unit. Belt-and-suspenders against include-glob variations:",
|
|
198
|
+
"// - Apps that include `.kumiko/` in tsconfig pick up the .d.ts",
|
|
199
|
+
"// transitively; the reference is redundant but harmless.",
|
|
200
|
+
"// - Tooling that compiles a narrow file-set (probes, isolated",
|
|
201
|
+
"// test programs) typically ignores .d.ts unless explicitly",
|
|
202
|
+
"// referenced — without this line, the augmentation is invisible",
|
|
203
|
+
"// and `keyof KumikoEventTypeMap` collapses to `never`. Verified",
|
|
204
|
+
"// empirically; see strict-mode-diagnostics.test.ts.",
|
|
205
|
+
'// NOT a `import "./types.generated"` side-effect — the file is .d.ts',
|
|
206
|
+
"// (declarations only), runtime tools (Vitest, Bun, Node) can't load",
|
|
207
|
+
"// it. Triple-slash is type-only, fully elided from JS output.",
|
|
208
|
+
`/// <reference path="./types.generated.d.ts" />`,
|
|
209
|
+
"",
|
|
210
|
+
"// Re-export the entire engine surface — apps can switch their imports",
|
|
211
|
+
"// from `@cosmicdrift/kumiko-framework/engine` to `./.kumiko/define` with a single",
|
|
212
|
+
"// sed-replace, no fine-grained import-splitting needed. The strict",
|
|
213
|
+
"// `defineWriteHandler` / `defineQueryHandler` overrides below shadow",
|
|
214
|
+
"// the loose framework versions in the local module's export table.",
|
|
215
|
+
`export * from "@cosmicdrift/kumiko-framework/engine";`,
|
|
216
|
+
"",
|
|
217
|
+
`import {`,
|
|
218
|
+
` defineWriteHandler as fwDefineWriteHandler,`,
|
|
219
|
+
` defineQueryHandler as fwDefineQueryHandler,`,
|
|
220
|
+
`} from "@cosmicdrift/kumiko-framework/engine";`,
|
|
221
|
+
`import type {`,
|
|
222
|
+
` KumikoEventTypeMap,`,
|
|
223
|
+
` WriteHandlerDefinition,`,
|
|
224
|
+
` WriteHandlerInput,`,
|
|
225
|
+
` QueryHandlerDefinition,`,
|
|
226
|
+
`} from "@cosmicdrift/kumiko-framework/engine";`,
|
|
227
|
+
`import type { ZodType } from "zod";`,
|
|
228
|
+
"",
|
|
229
|
+
`// Strict defineWriteHandler — TMap fixed to the global`,
|
|
230
|
+
`// KumikoEventTypeMap (which the augmentation extends). ctx.appendEvent`,
|
|
231
|
+
`// inside the handler resolves K against the FULL augmented map.`,
|
|
232
|
+
`export function defineWriteHandler<`,
|
|
233
|
+
` const TName extends string,`,
|
|
234
|
+
` TSchema extends ZodType,`,
|
|
235
|
+
` TData = unknown,`,
|
|
236
|
+
`>(`,
|
|
237
|
+
` def: WriteHandlerInput<TName, TSchema, TData, KumikoEventTypeMap>,`,
|
|
238
|
+
`): WriteHandlerDefinition<TName, TSchema, TData, KumikoEventTypeMap> {`,
|
|
239
|
+
` return fwDefineWriteHandler<TName, TSchema, TData, KumikoEventTypeMap>(def);`,
|
|
240
|
+
`}`,
|
|
241
|
+
"",
|
|
242
|
+
`export function defineQueryHandler<`,
|
|
243
|
+
` const TName extends string,`,
|
|
244
|
+
` TSchema extends ZodType,`,
|
|
245
|
+
` TResult = unknown,`,
|
|
246
|
+
`>(`,
|
|
247
|
+
` def: QueryHandlerDefinition<TName, TSchema, TResult, KumikoEventTypeMap>,`,
|
|
248
|
+
`): QueryHandlerDefinition<TName, TSchema, TResult, KumikoEventTypeMap> {`,
|
|
249
|
+
` return fwDefineQueryHandler<TName, TSchema, TResult, KumikoEventTypeMap>(def);`,
|
|
250
|
+
`}`,
|
|
251
|
+
typedDispatcherBlock,
|
|
252
|
+
].join("\n");
|
|
253
|
+
return body;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Render `WriteHandlerQn` type lines — exports a union of all registered
|
|
257
|
+
* write handler QNs so custom-screen code can type-check dispatcher.write
|
|
258
|
+
* calls at compile time:
|
|
259
|
+
*
|
|
260
|
+
* ```ts
|
|
261
|
+
* import type { WriteHandlerQn } from "@app/define";
|
|
262
|
+
* dispatcher.write<WriteHandlerQn>("tenant:write:create", payload); // ✓
|
|
263
|
+
* dispatcher.write<WriteHandlerQn>("tenant:write:creat", payload); // ✗ TS error
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export function renderWriteHandlerTypes(handlerQns) {
|
|
267
|
+
if (handlerQns.length === 0)
|
|
268
|
+
return "";
|
|
269
|
+
// Stable order — callers pass registration order (dev-server) or manifest
|
|
270
|
+
// order (CLI); bare sort keeps both byte-identical.
|
|
271
|
+
const lines = [...handlerQns].sort().map((qn) => ` | "${qn}"`);
|
|
272
|
+
return ["", WRITE_HANDLER_QN_MARKER, `export type WriteHandlerQn =`, ...lines, ";", ""].join("\n");
|
|
273
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ScanWarning } from "./scan-events.js";
|
|
2
|
+
export type CodegenOptions = {
|
|
3
|
+
/** App-Root — `<appRoot>/.kumiko/` ist der Output-Ordner. */
|
|
4
|
+
readonly appRoot: string;
|
|
5
|
+
/** Optional: alle registrierten Write-Handler-QNs. Wenn nicht gesetzt,
|
|
6
|
+
* versucht der Codegen `feature-manifest.json` im appRoot zu lesen.
|
|
7
|
+
* Die Dev-Server-Integration übergibt die QNs direkt aus der gebooteten
|
|
8
|
+
* Registry — CLI ruft sie aus dem Manifest. */
|
|
9
|
+
readonly handlerQns?: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export type CodegenResult = {
|
|
12
|
+
readonly outputDir: string;
|
|
13
|
+
readonly eventCount: number;
|
|
14
|
+
readonly warnings: readonly ScanWarning[];
|
|
15
|
+
readonly didWriteTypes: boolean;
|
|
16
|
+
readonly didWriteSchemas: boolean;
|
|
17
|
+
readonly didWriteDefine: boolean;
|
|
18
|
+
readonly skipped: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare function runCodegen(opts: CodegenOptions): CodegenResult;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// runCodegen — top-level entry point. Called by the dev-server (on boot,
|
|
2
|
+
// then on every watched file-change) and by `kumiko-build` (before
|
|
3
|
+
// bundling).
|
|
4
|
+
//
|
|
5
|
+
// Lifecycle:
|
|
6
|
+
// 1. Scan `<appRoot>/src/**` nach r.defineEvent.
|
|
7
|
+
// 2. Render bis zu drei Files unter `<appRoot>/.kumiko/`:
|
|
8
|
+
// - types.generated.d.ts (immer wenn Events oder bestehende Datei)
|
|
9
|
+
// - schemas.generated.ts (nur wenn ≥1 inline-Schema)
|
|
10
|
+
// - define.ts (immer)
|
|
11
|
+
// 3. Schreibe nur bei tatsächlicher Änderung — sonst kein touch
|
|
12
|
+
// (TS-Sprachserver bleibt cached, Watcher feuert nicht).
|
|
13
|
+
// 4. Wenn 0 Events UND noch kein .kumiko/ existiert: bail. Apps die
|
|
14
|
+
// `r.defineEvent` nicht nutzen brauchen keinen Wrapper-Pfad und
|
|
15
|
+
// kein leeres Verzeichnis.
|
|
16
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { renderDefineFile, renderInlineSchemasFile, renderTypesAugmentation, renderWriteHandlerTypes, TYPED_DISPATCHER_MARKER, WRITE_HANDLER_QN_MARKER, } from "./render.js";
|
|
19
|
+
import { scanEvents } from "./scan-events.js";
|
|
20
|
+
export function runCodegen(opts) {
|
|
21
|
+
const outputDir = join(opts.appRoot, ".kumiko");
|
|
22
|
+
const outputExists = existsSync(outputDir);
|
|
23
|
+
const scan = scanEvents({ appRoot: opts.appRoot });
|
|
24
|
+
const typesPath = join(outputDir, "types.generated.d.ts");
|
|
25
|
+
const definePath = join(outputDir, "define.ts");
|
|
26
|
+
const schemasPath = join(outputDir, "schemas.generated.ts");
|
|
27
|
+
const packageJsonPath = join(outputDir, "package.json");
|
|
28
|
+
// Skip-Pfad: keine Events gefunden + keine bestehende Output-Dir
|
|
29
|
+
// bedeutet die App nutzt r.defineEvent nicht (oder noch nicht). Kein
|
|
30
|
+
// leeres `.kumiko/` zurücklassen — das hilft niemandem und produziert
|
|
31
|
+
// false-positives in CI ("eh, was ist denn das hier"). Wenn die Dir
|
|
32
|
+
// schon existiert (alter Run), generieren wir trotzdem — ein Refactor
|
|
33
|
+
// der den letzten r.defineEvent löscht soll das Output dann auch
|
|
34
|
+
// bereinigen, statt eine stale Augmentation liegen zu lassen.
|
|
35
|
+
if (scan.events.length === 0 && !outputExists) {
|
|
36
|
+
return {
|
|
37
|
+
outputDir,
|
|
38
|
+
eventCount: 0,
|
|
39
|
+
warnings: scan.warnings,
|
|
40
|
+
didWriteTypes: false,
|
|
41
|
+
didWriteSchemas: false,
|
|
42
|
+
didWriteDefine: false,
|
|
43
|
+
skipped: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
mkdirSync(outputDir, { recursive: true });
|
|
47
|
+
const typesContent = renderTypesAugmentation(scan.events, outputDir);
|
|
48
|
+
// `opts.handlerQns` undefined means the caller (kumiko-build) has no
|
|
49
|
+
// live feature registry to draw from and relies on the manifest
|
|
50
|
+
// fallback. That fallback can't tell "no handlers exist" apart from
|
|
51
|
+
// "the manifest doesn't exist" — both come back as an empty array —
|
|
52
|
+
// so an empty result here is a known-lossy read, not authoritative
|
|
53
|
+
// data the way run-dev-app's live registry is.
|
|
54
|
+
const usingManifestFallback = opts.handlerQns === undefined;
|
|
55
|
+
const handlerQns = opts.handlerQns ?? readHandlerQnsFromManifest(opts.appRoot);
|
|
56
|
+
const hasHandlerQns = handlerQns.length > 0;
|
|
57
|
+
const defineContent = renderDefineFile(handlerQns);
|
|
58
|
+
const schemasContent = renderInlineSchemasFile(scan.events, opts.appRoot);
|
|
59
|
+
// package.json — turns `.kumiko/` into a real installable package
|
|
60
|
+
// named `@app/define`. Apps that declare
|
|
61
|
+
// "@app/define": "link:./.kumiko"
|
|
62
|
+
// in their package.json get a node_modules symlink that the runtime
|
|
63
|
+
// (Node, Vitest, Bun) all resolve via standard module-lookup. Yarn 4
|
|
64
|
+
// is required — yarn classic v1 ignored deps in versionless workspaces.
|
|
65
|
+
const packageJsonContent = renderKumikoPackageJson();
|
|
66
|
+
// WriteHandlerQn-Union an den types-Content anhängen (optional). Der
|
|
67
|
+
// Dev-Server übergibt handlerQns aus der lebenden Registry (genauer);
|
|
68
|
+
// der CLI-Fallback liest das feature-manifest.json falls vorhanden.
|
|
69
|
+
//
|
|
70
|
+
// Preserve only applies to the manifest-fallback path: a caller that
|
|
71
|
+
// explicitly passes `handlerQns: []` (the dev-server, when an app
|
|
72
|
+
// truly registers zero write handlers) means it — that's real data,
|
|
73
|
+
// not a missing-manifest gap, so it clears the block like before.
|
|
74
|
+
// Only the manifest fallback coming back empty is ambiguous enough to
|
|
75
|
+
// warrant carrying the previous block forward instead of dropping it.
|
|
76
|
+
const shouldPreserveStaleBlock = !hasHandlerQns && usingManifestFallback;
|
|
77
|
+
const finalTypesContent = shouldPreserveStaleBlock
|
|
78
|
+
? withPreservedBlock(typesContent, typesPath, WRITE_HANDLER_QN_MARKER, LEGACY_WRITE_HANDLER_QN_ANCHOR)
|
|
79
|
+
: `${typesContent}${renderWriteHandlerTypes(handlerQns)}`;
|
|
80
|
+
const finalDefineContent = shouldPreserveStaleBlock
|
|
81
|
+
? withPreservedBlock(defineContent, definePath, TYPED_DISPATCHER_MARKER, LEGACY_TYPED_DISPATCHER_ANCHOR)
|
|
82
|
+
: defineContent;
|
|
83
|
+
const didWriteTypes = writeIfChanged(typesPath, finalTypesContent);
|
|
84
|
+
const didWriteDefine = writeIfChanged(definePath, finalDefineContent);
|
|
85
|
+
writeIfChanged(packageJsonPath, packageJsonContent);
|
|
86
|
+
const didWriteSchemas = schemasContent !== undefined
|
|
87
|
+
? writeIfChanged(schemasPath, schemasContent)
|
|
88
|
+
: removeIfExists(schemasPath);
|
|
89
|
+
const warnings = shouldPreserveStaleBlock
|
|
90
|
+
? [
|
|
91
|
+
...scan.warnings,
|
|
92
|
+
{
|
|
93
|
+
message: "No feature-manifest.json found — the WriteHandlerQn/TypedDispatcher block could not be determined from here; run the dev-server to get the exact handler list.",
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
: scan.warnings;
|
|
97
|
+
return {
|
|
98
|
+
outputDir,
|
|
99
|
+
eventCount: scan.events.length,
|
|
100
|
+
warnings,
|
|
101
|
+
didWriteTypes,
|
|
102
|
+
didWriteSchemas,
|
|
103
|
+
didWriteDefine,
|
|
104
|
+
skipped: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Static package.json content — turns `.kumiko/` into an installable
|
|
109
|
+
* package called `@app/define`. The shape never depends on event-scans,
|
|
110
|
+
* so we don't bother passing the events in.
|
|
111
|
+
*
|
|
112
|
+
* Two things to keep stable:
|
|
113
|
+
* - `name: "@app/define"` matches what handler imports use.
|
|
114
|
+
* - `exports."."` points at the wrapper, `exports."./*"` lets apps
|
|
115
|
+
* reach into types.generated etc. via `@app/types.generated`.
|
|
116
|
+
*/
|
|
117
|
+
function renderKumikoPackageJson() {
|
|
118
|
+
const pkg = {
|
|
119
|
+
name: "@app/define",
|
|
120
|
+
private: true,
|
|
121
|
+
// license: keep the generated package out of unknown-license territory
|
|
122
|
+
// for the License-Check guard. The repo is BUSL-1.1-licensed, the generated
|
|
123
|
+
// wrapper inherits that — the file just re-exports framework code,
|
|
124
|
+
// there's no original IP in `.kumiko/` worth a different license.
|
|
125
|
+
license: "BUSL-1.1",
|
|
126
|
+
version: "0.0.0",
|
|
127
|
+
type: "module",
|
|
128
|
+
main: "./define.ts",
|
|
129
|
+
types: "./define.ts",
|
|
130
|
+
exports: {
|
|
131
|
+
".": "./define.ts",
|
|
132
|
+
"./*": "./*",
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Idempotent write — only touches the file when its content actually
|
|
139
|
+
* changed. Critical for the dev-server watcher: a no-op codegen pass
|
|
140
|
+
* must NOT trigger a full TS-language-server rebuild (which would
|
|
141
|
+
* happen on every mtime change).
|
|
142
|
+
*/
|
|
143
|
+
function writeIfChanged(path, content) {
|
|
144
|
+
const existing = readExistingOrUndefined(path);
|
|
145
|
+
if (existing === content)
|
|
146
|
+
return false;
|
|
147
|
+
writeFileSync(path, content, "utf-8");
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
function readExistingOrUndefined(path) {
|
|
151
|
+
try {
|
|
152
|
+
return readFileSync(path, "utf-8");
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Pre-marker renderer output has no WRITE_HANDLER_QN_MARKER/TYPED_DISPATCHER_MARKER
|
|
159
|
+
// line to find — a file generated before this fix still needs to round-trip.
|
|
160
|
+
// Each anchor is the first line of its block under the old renderer, so slicing
|
|
161
|
+
// from the preceding newline recovers the same block the marker path recovers.
|
|
162
|
+
const LEGACY_WRITE_HANDLER_QN_ANCHOR = `export type WriteHandlerQn =`;
|
|
163
|
+
const LEGACY_TYPED_DISPATCHER_ANCHOR = `import type { Dispatcher, WriteOpts, WriteResult } from "@cosmicdrift/kumiko-headless";`;
|
|
164
|
+
/**
|
|
165
|
+
* Appends whichever handler-derived block a previous run left behind
|
|
166
|
+
* in `existingPath`, found via its start-of-block `marker` (or, for a
|
|
167
|
+
* file generated before markers existed, `legacyAnchor`). Used when
|
|
168
|
+
* the current run has no `handlerQns` of its own — the block can't be
|
|
169
|
+
* re-derived, so the only options are "delete it" (the #2757 bug) or
|
|
170
|
+
* "leave it exactly as it was". The marker is always emitted as the
|
|
171
|
+
* last thing its renderer produces, so slicing from it to EOF recovers
|
|
172
|
+
* the whole block regardless of how many times this preserve-path runs
|
|
173
|
+
* in a row.
|
|
174
|
+
*/
|
|
175
|
+
function withPreservedBlock(freshContent, existingPath, marker, legacyAnchor) {
|
|
176
|
+
const existing = readExistingOrUndefined(existingPath);
|
|
177
|
+
const preserved = extractBlock(existing, marker) ?? extractBlock(existing, legacyAnchor);
|
|
178
|
+
return preserved !== undefined ? `${freshContent}${preserved}` : freshContent;
|
|
179
|
+
}
|
|
180
|
+
function extractBlock(content, anchor) {
|
|
181
|
+
if (content === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
const idx = content.indexOf(`\n${anchor}`);
|
|
184
|
+
return idx === -1 ? undefined : content.slice(idx);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Remove a previously-generated file when the latest scan no longer
|
|
188
|
+
* produces it (e.g. the last inline-schema was refactored to a named
|
|
189
|
+
* export). Returns true when an actual unlink happened.
|
|
190
|
+
*/
|
|
191
|
+
function removeIfExists(path) {
|
|
192
|
+
if (!existsSync(path))
|
|
193
|
+
return false;
|
|
194
|
+
rmSync(path, { force: true });
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Fallback: liest `feature-manifest.json` aus dem appRoot und extrahiert
|
|
199
|
+
* alle Write-Handler-QNs. Fehlende/stale Manifeste sind kein Fehler —
|
|
200
|
+
* dann wird schlicht keine `WriteHandlerQn`-Union generiert (CLI ohne
|
|
201
|
+
* Manifest oder CI-Setup ohne Manifest-Generator).
|
|
202
|
+
*/
|
|
203
|
+
function readHandlerQnsFromManifest(appRoot) {
|
|
204
|
+
try {
|
|
205
|
+
const manifestPath = join(appRoot, "feature-manifest.json");
|
|
206
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
207
|
+
const manifest = JSON.parse(raw);
|
|
208
|
+
const qns = [];
|
|
209
|
+
for (const f of manifest.features) {
|
|
210
|
+
if (f.writeHandlers)
|
|
211
|
+
qns.push(...f.writeHandlers);
|
|
212
|
+
}
|
|
213
|
+
qns.sort();
|
|
214
|
+
return qns;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type ScannedEvent = {
|
|
2
|
+
/** Qualified event-name as it appears in the events table:
|
|
3
|
+
* `<feature>:event:<inner>`. The KumikoEventTypeMap key. */
|
|
4
|
+
readonly qualifiedName: string;
|
|
5
|
+
/** Where the type for this event's payload comes from. Two flavours:
|
|
6
|
+
*
|
|
7
|
+
* { kind: "imported", schemaIdentifier, schemaModulePath }
|
|
8
|
+
* Schema is a named export of another file. Augmentation
|
|
9
|
+
* `import type`-s it from there.
|
|
10
|
+
*
|
|
11
|
+
* { kind: "inline", schemaSource, generatedConstName }
|
|
12
|
+
* Schema was inlined at the call-site. We extract the source
|
|
13
|
+
* text into `schemas.generated.ts` under `generatedConstName`
|
|
14
|
+
* and import-type from there. */
|
|
15
|
+
readonly schemaSource: SchemaSource;
|
|
16
|
+
/** Absolute disk-path of the feature file (for relative-path
|
|
17
|
+
* resolution + diagnostics). */
|
|
18
|
+
readonly featureFilePath: string;
|
|
19
|
+
/** For diagnostics + dedup logging. */
|
|
20
|
+
readonly source: {
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly line: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export type SchemaSource = {
|
|
26
|
+
readonly kind: "imported";
|
|
27
|
+
readonly schemaIdentifier: string;
|
|
28
|
+
readonly schemaModulePath: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly kind: "inline";
|
|
31
|
+
/** zod source text — `z.object({ id: z.string() })` etc. */
|
|
32
|
+
readonly schemaSource: string;
|
|
33
|
+
/** Name we'll generate inside `schemas.generated.ts`. Stable
|
|
34
|
+
* + qualified-name-derived so reorder of features doesn't
|
|
35
|
+
* rename them. */
|
|
36
|
+
readonly generatedConstName: string;
|
|
37
|
+
};
|
|
38
|
+
export type ScanWarning = {
|
|
39
|
+
/** Absent for warnings that aren't tied to a scanned source line
|
|
40
|
+
* (e.g. a codegen-level warning about a missing manifest file). */
|
|
41
|
+
readonly file?: string;
|
|
42
|
+
readonly line?: number;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
};
|
|
45
|
+
export type ScanResult = {
|
|
46
|
+
readonly events: readonly ScannedEvent[];
|
|
47
|
+
readonly warnings: readonly ScanWarning[];
|
|
48
|
+
};
|
|
49
|
+
/** Renders a `ScanWarning` for terminal output; omits the location prefix when the warning has no source line. */
|
|
50
|
+
export declare function formatScanWarning(warning: ScanWarning): string;
|
|
51
|
+
export type ScanOptions = {
|
|
52
|
+
/** App-Wurzel — alles unter `<root>/src` wird gescannt. Tests +
|
|
53
|
+
* generated-files (`.kumiko`, `dist*`, `node_modules`) sind raus. */
|
|
54
|
+
readonly appRoot: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Scant `appRoot/src/**` nach `r.defineEvent(...)`-Aufrufen und liefert
|
|
58
|
+
* eine deduplizierte Liste aus ScannedEvent. Doppelte qualifiedName
|
|
59
|
+
* landen mit einer Warnung in der Result — Codegen schreibt nur den
|
|
60
|
+
* ERSTEN, sodass das generated File compile-stabil bleibt.
|
|
61
|
+
*/
|
|
62
|
+
export declare function scanEvents(opts: ScanOptions): ScanResult;
|
|
63
|
+
/**
|
|
64
|
+
* Stable identifier-safe rewrite of a qualifiedName like
|
|
65
|
+
* `pubsubOrders:event:order-placed` → `_kg_pubsubOrders__orderPlaced`.
|
|
66
|
+
* Used for the `export const` name in `schemas.generated.ts`. Same
|
|
67
|
+
* transform in scan + render keeps the two sides in sync.
|
|
68
|
+
*/
|
|
69
|
+
export declare function qualifiedNameToConstName(qualifiedName: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the relative schema-import path from the feature-file's
|
|
72
|
+
* point-of-view to a path relative to `.kumiko/` (which is where the
|
|
73
|
+
* generated d.ts file lives). Workspace-package specifiers
|
|
74
|
+
* (`@cosmicdrift/kumiko-...`) are returned as-is.
|
|
75
|
+
*/
|
|
76
|
+
export declare function rewriteImportPath(schemaModulePath: string, featureFilePath: string, outputDirAbs: string): string;
|