@cosmicdrift/kumiko-dev-server 0.1.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 (44) hide show
  1. package/bin/kumiko-build.ts +85 -0
  2. package/bin/kumiko-dev.ts +90 -0
  3. package/package.json +45 -0
  4. package/src/__tests__/build-prod-bundle.integration.ts +265 -0
  5. package/src/__tests__/build-prod-bundle.test.ts +262 -0
  6. package/src/__tests__/cache-headers.test.ts +70 -0
  7. package/src/__tests__/classify-change.test.ts +87 -0
  8. package/src/__tests__/compose-features-wiring.integration.ts +352 -0
  9. package/src/__tests__/compose-features.test.ts +81 -0
  10. package/src/__tests__/crash-tracker.test.ts +89 -0
  11. package/src/__tests__/create-kumiko-server.integration.ts +286 -0
  12. package/src/__tests__/few-shot-corpus.test.ts +311 -0
  13. package/src/__tests__/inject-schema.test.ts +62 -0
  14. package/src/__tests__/resolve-stylesheet.test.ts +90 -0
  15. package/src/__tests__/resolve-tailwind-cli.test.ts +49 -0
  16. package/src/__tests__/run-prod-app-spec.test.ts +57 -0
  17. package/src/__tests__/run-prod-app.integration.ts +535 -0
  18. package/src/__tests__/scaffold-feature.test.ts +143 -0
  19. package/src/__tests__/try-hono-first.test.ts +63 -0
  20. package/src/build-prod-bundle.ts +587 -0
  21. package/src/build-server-bundle.ts +308 -0
  22. package/src/build.ts +28 -0
  23. package/src/codegen/__tests__/run-codegen.test.ts +494 -0
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +467 -0
  25. package/src/codegen/__tests__/watch.test.ts +186 -0
  26. package/src/codegen/index.ts +17 -0
  27. package/src/codegen/render.ts +225 -0
  28. package/src/codegen/run-codegen.ts +157 -0
  29. package/src/codegen/scan-events.ts +574 -0
  30. package/src/codegen/watch.ts +127 -0
  31. package/src/compose-features.ts +128 -0
  32. package/src/crash-tracker.ts +56 -0
  33. package/src/create-kumiko-server.ts +1010 -0
  34. package/src/drizzle-config.ts +44 -0
  35. package/src/drizzle-tables-auth-mode.ts +32 -0
  36. package/src/drizzle-tables-minimal.ts +22 -0
  37. package/src/few-shot-corpus.ts +369 -0
  38. package/src/index.ts +57 -0
  39. package/src/inject-schema.ts +24 -0
  40. package/src/resolve-tailwind-cli.ts +28 -0
  41. package/src/run-dev-app.ts +290 -0
  42. package/src/run-prod-app.ts +892 -0
  43. package/src/scaffold-feature.ts +226 -0
  44. package/src/try-hono-first.ts +46 -0
@@ -0,0 +1,225 @@
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
+
25
+ import type { ScannedEvent } from "./scan-events";
26
+ import { rewriteImportPath } from "./scan-events";
27
+
28
+ const HEADER = [
29
+ "// =====================================================================",
30
+ "// AUTO-GENERATED — DO NOT EDIT BY HAND",
31
+ "// Run `yarn kumiko codegen` to regenerate (or rely on the dev-server's",
32
+ "// file-watcher, which calls it on every r.defineEvent change).",
33
+ "// =====================================================================",
34
+ ].join("\n");
35
+
36
+ /**
37
+ * Render `types.generated.d.ts`. Imports the schema-identifiers as
38
+ * type-only — sources can be either named exports of feature-events
39
+ * files (kind: "imported") or extracted const-exports of the
40
+ * co-generated schemas.generated.ts (kind: "inline"). Result is wired
41
+ * via `z.infer<typeof X>` into the augmentation. Empty events list →
42
+ * minimal but valid file (still augmentable, just no entries yet —
43
+ * useful first-time scaffold).
44
+ */
45
+ export function renderTypesAugmentation(
46
+ events: readonly ScannedEvent[],
47
+ outputDirAbs: string,
48
+ ): string {
49
+ // Group identifiers by their resolved (rewritten) module path so each
50
+ // schema file imports once. Multiple events sharing the same events.ts
51
+ // file is the common case for the "imported" kind. Inline-events all
52
+ // share the same module path: "./schemas.generated".
53
+ const importsByPath = new Map<string, Set<string>>();
54
+ for (const ev of events) {
55
+ if (ev.schemaSource.kind === "imported") {
56
+ const rewritten = rewriteImportPath(
57
+ ev.schemaSource.schemaModulePath,
58
+ ev.featureFilePath,
59
+ outputDirAbs,
60
+ );
61
+ if (!importsByPath.has(rewritten)) importsByPath.set(rewritten, new Set());
62
+ importsByPath.get(rewritten)?.add(ev.schemaSource.schemaIdentifier);
63
+ } else {
64
+ const path = "./schemas.generated";
65
+ if (!importsByPath.has(path)) importsByPath.set(path, new Set());
66
+ importsByPath.get(path)?.add(ev.schemaSource.generatedConstName);
67
+ }
68
+ }
69
+
70
+ const importLines: string[] = [];
71
+ // `z` from zod is needed for `z.infer<typeof X>`; we import it once.
72
+ importLines.push(`import type { z } from "zod";`);
73
+ // Stable order — sort module paths alphabetically; identifiers within
74
+ // a module also alphabetically. Idempotent output.
75
+ for (const [modPath, idents] of [...importsByPath.entries()].sort(([a], [b]) =>
76
+ a.localeCompare(b),
77
+ )) {
78
+ const sortedIdents = [...idents].sort();
79
+ importLines.push(`import type { ${sortedIdents.join(", ")} } from "${modPath}";`);
80
+ }
81
+
82
+ const mapEntries = [...events]
83
+ .sort((a, b) => a.qualifiedName.localeCompare(b.qualifiedName))
84
+ .map((ev) => {
85
+ const refName =
86
+ ev.schemaSource.kind === "imported"
87
+ ? ev.schemaSource.schemaIdentifier
88
+ : ev.schemaSource.generatedConstName;
89
+ return ` "${ev.qualifiedName}": z.infer<typeof ${refName}>;`;
90
+ });
91
+
92
+ // Body block — `declare module` in module-form (because we have an
93
+ // `export {}` at the end). Module-form makes the augmentation merge
94
+ // into the target interface; a script-form (`export {}` removed)
95
+ // would REPLACE the module's exports.
96
+ const body = [
97
+ 'declare module "@cosmicdrift/kumiko-framework/engine" {',
98
+ " interface KumikoEventTypeMap {",
99
+ ...(mapEntries.length === 0 ? [" // (no r.defineEvent calls discovered yet)"] : mapEntries),
100
+ " }",
101
+ "}",
102
+ "",
103
+ "export {};",
104
+ "",
105
+ ].join("\n");
106
+
107
+ return [HEADER, "", ...importLines, "", body].join("\n");
108
+ }
109
+
110
+ /**
111
+ * Render `schemas.generated.ts`. One `export const` per inline-schema
112
+ * event, named via the qualifiedName-derived stable identifier. The
113
+ * source-text of the original `z.*(...)` expression is replayed — we
114
+ * don't try to be cleverer than the zod compiler about reconstructing
115
+ * the schema. Returns undefined when no inline-schemas exist (so the
116
+ * runner can skip writing the file entirely).
117
+ */
118
+ export function renderInlineSchemasFile(events: readonly ScannedEvent[]): string | undefined {
119
+ const inlines = events.filter((ev) => ev.schemaSource.kind === "inline");
120
+ if (inlines.length === 0) return undefined;
121
+
122
+ const lines: string[] = [
123
+ HEADER,
124
+ "",
125
+ "// Schema extracts purely for type inference: this file is referenced",
126
+ "// from types.generated.d.ts via `import type`. ts-strip elides it at",
127
+ "// build time, so there is NO runtime duplication of the inline schemas",
128
+ "// in feature files. When an event schema changes: re-run `yarn kumiko",
129
+ "// codegen` — otherwise the z.infer type drifts from the runtime schema.",
130
+ "",
131
+ `import { z } from "zod";`,
132
+ "",
133
+ ];
134
+ // Sort by const-name for stable output.
135
+ const sorted = [...inlines].sort((a, b) => {
136
+ const aName = a.schemaSource.kind === "inline" ? a.schemaSource.generatedConstName : "";
137
+ const bName = b.schemaSource.kind === "inline" ? b.schemaSource.generatedConstName : "";
138
+ return aName.localeCompare(bName);
139
+ });
140
+ for (const ev of sorted) {
141
+ if (ev.schemaSource.kind !== "inline") continue;
142
+ lines.push(
143
+ `// ${ev.qualifiedName} — from ${ev.featureFilePath}:${ev.source.line}`,
144
+ `export const ${ev.schemaSource.generatedConstName} = ${ev.schemaSource.schemaSource};`,
145
+ "",
146
+ );
147
+ }
148
+ return lines.join("\n");
149
+ }
150
+
151
+ /**
152
+ * Render `define.ts` — local thin wrappers that fix TMap to
153
+ * KumikoEventTypeMap. Apps import `defineWriteHandler` /
154
+ * `defineQueryHandler` from this file; framework's strict-overload
155
+ * becomes the only matching overload because TMap is no longer the
156
+ * eager-resolved default.
157
+ *
158
+ * The wrappers are intentionally *thin* — same signature as the
159
+ * framework's, just with TMap pre-bound. Apps stay portable: switch
160
+ * the import back to "@cosmicdrift/kumiko-framework/engine" and you're back on the
161
+ * loose default. Migration is reversible, no behavioural surface change.
162
+ */
163
+ export function renderDefineFile(): string {
164
+ const body = [
165
+ HEADER,
166
+ "",
167
+ "// Triple-slash reference pulls the augmentation into this compile-",
168
+ "// unit. Belt-and-suspenders against include-glob variations:",
169
+ "// - Apps that include `.kumiko/` in tsconfig pick up the .d.ts",
170
+ "// transitively; the reference is redundant but harmless.",
171
+ "// - Tooling that compiles a narrow file-set (probes, isolated",
172
+ "// test programs) typically ignores .d.ts unless explicitly",
173
+ "// referenced — without this line, the augmentation is invisible",
174
+ "// and `keyof KumikoEventTypeMap` collapses to `never`. Verified",
175
+ "// empirically; see strict-mode-diagnostics.test.ts.",
176
+ '// NOT a `import "./types.generated"` side-effect — the file is .d.ts',
177
+ "// (declarations only), runtime tools (Vitest, Bun, Node) can't load",
178
+ "// it. Triple-slash is type-only, fully elided from JS output.",
179
+ `/// <reference path="./types.generated.d.ts" />`,
180
+ "",
181
+ "// Re-export the entire engine surface — apps can switch their imports",
182
+ "// from `@cosmicdrift/kumiko-framework/engine` to `./.kumiko/define` with a single",
183
+ "// sed-replace, no fine-grained import-splitting needed. The strict",
184
+ "// `defineWriteHandler` / `defineQueryHandler` overrides below shadow",
185
+ "// the loose framework versions in the local module's export table.",
186
+ `export * from "@cosmicdrift/kumiko-framework/engine";`,
187
+ "",
188
+ `import {`,
189
+ ` defineWriteHandler as fwDefineWriteHandler,`,
190
+ ` defineQueryHandler as fwDefineQueryHandler,`,
191
+ `} from "@cosmicdrift/kumiko-framework/engine";`,
192
+ `import type {`,
193
+ ` KumikoEventTypeMap,`,
194
+ ` WriteHandlerDefinition,`,
195
+ ` QueryHandlerDefinition,`,
196
+ `} from "@cosmicdrift/kumiko-framework/engine";`,
197
+ `import type { ZodType } from "zod";`,
198
+ "",
199
+ `// Strict defineWriteHandler — TMap fixed to the global`,
200
+ `// KumikoEventTypeMap (which the augmentation extends). ctx.appendEvent`,
201
+ `// inside the handler resolves K against the FULL augmented map.`,
202
+ `export function defineWriteHandler<`,
203
+ ` const TName extends string,`,
204
+ ` TSchema extends ZodType,`,
205
+ ` TData = unknown,`,
206
+ `>(`,
207
+ ` def: WriteHandlerDefinition<TName, TSchema, TData, KumikoEventTypeMap>,`,
208
+ `): WriteHandlerDefinition<TName, TSchema, TData, KumikoEventTypeMap> {`,
209
+ ` return fwDefineWriteHandler<TName, TSchema, TData, KumikoEventTypeMap>(def);`,
210
+ `}`,
211
+ "",
212
+ `export function defineQueryHandler<`,
213
+ ` const TName extends string,`,
214
+ ` TSchema extends ZodType,`,
215
+ ` TResult = unknown,`,
216
+ `>(`,
217
+ ` def: QueryHandlerDefinition<TName, TSchema, TResult, KumikoEventTypeMap>,`,
218
+ `): QueryHandlerDefinition<TName, TSchema, TResult, KumikoEventTypeMap> {`,
219
+ ` return fwDefineQueryHandler<TName, TSchema, TResult, KumikoEventTypeMap>(def);`,
220
+ `}`,
221
+ "",
222
+ ].join("\n");
223
+
224
+ return body;
225
+ }
@@ -0,0 +1,157 @@
1
+ // runCodegen — Top-Level Entry-Point. Wird vom Dev-Server (auf Boot),
2
+ // vom Build-Step (vor Bundle) und von der CLI (`yarn kumiko codegen`)
3
+ // aufgerufen.
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
+
17
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { renderDefineFile, renderInlineSchemasFile, renderTypesAugmentation } from "./render";
20
+ import { type ScanWarning, scanEvents } from "./scan-events";
21
+
22
+ export type CodegenOptions = {
23
+ /** App-Root — `<appRoot>/.kumiko/` ist der Output-Ordner. */
24
+ readonly appRoot: string;
25
+ };
26
+
27
+ export type CodegenResult = {
28
+ readonly outputDir: string;
29
+ readonly eventCount: number;
30
+ readonly warnings: readonly ScanWarning[];
31
+ readonly didWriteTypes: boolean;
32
+ readonly didWriteSchemas: boolean;
33
+ readonly didWriteDefine: boolean;
34
+ readonly skipped: boolean;
35
+ };
36
+
37
+ export function runCodegen(opts: CodegenOptions): CodegenResult {
38
+ const outputDir = join(opts.appRoot, ".kumiko");
39
+ const outputExists = existsSync(outputDir);
40
+
41
+ const scan = scanEvents({ appRoot: opts.appRoot });
42
+
43
+ const typesPath = join(outputDir, "types.generated.d.ts");
44
+ const definePath = join(outputDir, "define.ts");
45
+ const schemasPath = join(outputDir, "schemas.generated.ts");
46
+ const packageJsonPath = join(outputDir, "package.json");
47
+
48
+ // Skip-Pfad: keine Events gefunden + keine bestehende Output-Dir
49
+ // bedeutet die App nutzt r.defineEvent nicht (oder noch nicht). Kein
50
+ // leeres `.kumiko/` zurücklassen — das hilft niemandem und produziert
51
+ // false-positives in CI ("eh, was ist denn das hier"). Wenn die Dir
52
+ // schon existiert (alter Run), generieren wir trotzdem — ein Refactor
53
+ // der den letzten r.defineEvent löscht soll das Output dann auch
54
+ // bereinigen, statt eine stale Augmentation liegen zu lassen.
55
+ if (scan.events.length === 0 && !outputExists) {
56
+ return {
57
+ outputDir,
58
+ eventCount: 0,
59
+ warnings: scan.warnings,
60
+ didWriteTypes: false,
61
+ didWriteSchemas: false,
62
+ didWriteDefine: false,
63
+ skipped: true,
64
+ };
65
+ }
66
+
67
+ mkdirSync(outputDir, { recursive: true });
68
+
69
+ const typesContent = renderTypesAugmentation(scan.events, outputDir);
70
+ const defineContent = renderDefineFile();
71
+ const schemasContent = renderInlineSchemasFile(scan.events);
72
+ // package.json — turns `.kumiko/` into a real installable package
73
+ // named `@app/define`. Apps that declare
74
+ // "@app/define": "link:./.kumiko"
75
+ // in their package.json get a node_modules symlink that the runtime
76
+ // (Node, Vitest, Bun) all resolve via standard module-lookup. Yarn 4
77
+ // is required — yarn classic v1 ignored deps in versionless workspaces.
78
+ const packageJsonContent = renderKumikoPackageJson();
79
+
80
+ const didWriteTypes = writeIfChanged(typesPath, typesContent);
81
+ const didWriteDefine = writeIfChanged(definePath, defineContent);
82
+ writeIfChanged(packageJsonPath, packageJsonContent);
83
+ const didWriteSchemas =
84
+ schemasContent !== undefined
85
+ ? writeIfChanged(schemasPath, schemasContent)
86
+ : removeIfExists(schemasPath);
87
+
88
+ return {
89
+ outputDir,
90
+ eventCount: scan.events.length,
91
+ warnings: scan.warnings,
92
+ didWriteTypes,
93
+ didWriteSchemas,
94
+ didWriteDefine,
95
+ skipped: false,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Static package.json content — turns `.kumiko/` into an installable
101
+ * package called `@app/define`. The shape never depends on event-scans,
102
+ * so we don't bother passing the events in.
103
+ *
104
+ * Two things to keep stable:
105
+ * - `name: "@app/define"` matches what handler imports use.
106
+ * - `exports."."` points at the wrapper, `exports."./*"` lets apps
107
+ * reach into types.generated etc. via `@app/types.generated`.
108
+ */
109
+ function renderKumikoPackageJson(): string {
110
+ const pkg = {
111
+ name: "@app/define",
112
+ private: true,
113
+ // license: keep the generated package out of unknown-license territory
114
+ // for the License-Check guard. The repo is BUSL-1.1-licensed, the generated
115
+ // wrapper inherits that — the file just re-exports framework code,
116
+ // there's no original IP in `.kumiko/` worth a different license.
117
+ license: "BUSL-1.1",
118
+ version: "0.0.0",
119
+ type: "module",
120
+ main: "./define.ts",
121
+ types: "./define.ts",
122
+ exports: {
123
+ ".": "./define.ts",
124
+ "./*": "./*",
125
+ },
126
+ };
127
+ return `${JSON.stringify(pkg, null, 2)}\n`;
128
+ }
129
+
130
+ /**
131
+ * Idempotent write — only touches the file when its content actually
132
+ * changed. Critical for the dev-server watcher: a no-op codegen pass
133
+ * must NOT trigger a full TS-language-server rebuild (which would
134
+ * happen on every mtime change).
135
+ */
136
+ function writeIfChanged(path: string, content: string): boolean {
137
+ let existing: string | undefined;
138
+ try {
139
+ existing = readFileSync(path, "utf-8");
140
+ } catch {
141
+ existing = undefined;
142
+ }
143
+ if (existing === content) return false;
144
+ writeFileSync(path, content, "utf-8");
145
+ return true;
146
+ }
147
+
148
+ /**
149
+ * Remove a previously-generated file when the latest scan no longer
150
+ * produces it (e.g. the last inline-schema was refactored to a named
151
+ * export). Returns true when an actual unlink happened.
152
+ */
153
+ function removeIfExists(path: string): boolean {
154
+ if (!existsSync(path)) return false;
155
+ rmSync(path, { force: true });
156
+ return true;
157
+ }