@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,308 @@
1
+ // buildServerBundle — Production-Server-Bundle für Kumiko-Apps. Pendant
2
+ // zu buildProdBundle (Client). Convention-Driven Discovery liest
3
+ // bin/main.ts + optional drizzle/migration-hooks.ts und produziert ein
4
+ // runtime-self-contained dist-server/ aus dem ein Bun-Alpine-Container
5
+ // ohne Monorepo bootet.
6
+ //
7
+ // Convention:
8
+ //
9
+ // bin/main.ts → dist-server/server.js
10
+ // (App-Boot, ruft runProdApp)
11
+ // <repo>/bin/kumiko.ts → dist-server/kumiko.js
12
+ // (Migrate-CLI für Pre-Deploy-Step,
13
+ // gefunden durch walk-up bis bin/kumiko.ts)
14
+ // drizzle/migration-hooks.ts → dist-server/migration-hooks.js
15
+ // (optional — Projection-Rebuild-Marker-
16
+ // Reader, wird via KUMIKO_MIGRATION_HOOKS
17
+ // env-var von der CLI gefunden)
18
+ // drizzle.config.ts → dist-server/drizzle.config.ts
19
+ // (drizzle-kit lädt's per Convention.
20
+ // kumikoDrizzleConfig-Helper-Imports
21
+ // werden inlined damit der Container
22
+ // kein @cosmicdrift/kumiko-dev-server-Paket
23
+ // installiert haben muss.)
24
+ // dist-server/package.json → generiert mit Versionen aus framework +
25
+ // bundled-features package.json
26
+ //
27
+ // Output:
28
+ //
29
+ // dist-server/
30
+ // server.js ← App-Boot-Entry (~1 MB)
31
+ // kumiko.js ← Migrate-CLI (~1 MB)
32
+ // migration-hooks.js ← Rebuild-Marker-Reader (optional, ~1 MB)
33
+ // drizzle.config.ts ← gebundelter drizzle-kit-Config (~10 KB)
34
+ // package.json ← runtime-deps mit gepinnten Versionen
35
+ //
36
+ // Externals — Pakete die NICHT ins Bundle gebakt werden, sondern via
37
+ // `bun install` im Runtime-Container nachgeholt werden:
38
+ //
39
+ // RUNTIME_EXTERNALS — wird zur Laufzeit gebraucht (native bindings,
40
+ // worker-threads, dynamic-require). Landet in
41
+ // dist-server/package.json#dependencies.
42
+ //
43
+ // BUILD_ONLY_EXTERNALS — referenziert nur transitiv im Framework, vom
44
+ // App-Code aber nie. Tree-Shake wirft sie aus
45
+ // dem Bundle, der external-Marker schaltet nur
46
+ // das resolution-during-build ab. NICHT in
47
+ // runtime-deps.
48
+ //
49
+ // Beide Listen leben hier zentral statt pro-App. Wenn eine App ein neues
50
+ // natively-bound Paket nutzt, fällt es heute in die App-eigene Boilerplate
51
+ // (oder via opts.extraRuntimeExternals) — das wird sich mit
52
+ // auto-Detection entwickeln.
53
+
54
+ import { existsSync, readFileSync } from "node:fs";
55
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
56
+ import { dirname, join, resolve } from "node:path";
57
+
58
+ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
59
+
60
+ // Pakete die das Bundle zur Laufzeit weiter referenziert (`from "<pkg>"`
61
+ // nach Tree-Shake). Native bindings (argon2), worker-thread-Loader (bullmq,
62
+ // ioredis), dynamic-require (postgres, temporal-polyfill) — alles was
63
+ // unter Bun-bundling bricht.
64
+ //
65
+ // `drizzle-kit` + `drizzle-orm` sind im Bundle 0 Refs (drizzle-orm wird
66
+ // inline gebakt), liegen aber in runtime-deps damit der Pre-Deploy-
67
+ // Migrate-Step (`bun /app/kumiko.js migrate apply`) drizzle-kit als CLI
68
+ // findet. drizzle-kit prüft beim Start ob drizzle-orm separat installiert
69
+ // ist und exit'tet sonst mit "Please install latest version of drizzle-orm".
70
+ const RUNTIME_EXTERNALS = [
71
+ "@node-rs/argon2",
72
+ "bullmq",
73
+ "drizzle-kit",
74
+ "drizzle-orm",
75
+ "ioredis",
76
+ "postgres",
77
+ "temporal-polyfill",
78
+ ] as const;
79
+
80
+ // Pakete die nur im Build-Stack erscheinen (transitive Imports im
81
+ // Framework), aber vom App-Code nicht referenziert werden. Ohne external-
82
+ // Markierung scheitert bun build an dynamic-imports (z.B. drizzle-kit →
83
+ // @libsql/client). Tree-Shake wirft sie eh aus dem Bundle — der Marker
84
+ // schaltet nur das resolution-during-build ab. NICHT in runtime-deps.
85
+ const BUILD_ONLY_EXTERNALS = ["meilisearch", "pino", "pino-pretty", "@aws-sdk/*"] as const;
86
+
87
+ export type BuildServerBundleOptions = {
88
+ /** App-Root. Default: process.cwd(). */
89
+ readonly cwd?: string;
90
+ /** Output-Folder relativ zu cwd. Default: "dist-server". */
91
+ readonly outDir?: string;
92
+ /** Zusätzliche Pakete die zur Laufzeit installiert sein müssen
93
+ * (beyond RUNTIME_EXTERNALS). App-spezifisch — Default leer. */
94
+ readonly extraRuntimeExternals?: readonly string[];
95
+ /** Zusätzliche Build-only-Externals. App-spezifisch — Default leer. */
96
+ readonly extraBuildOnlyExternals?: readonly string[];
97
+ };
98
+
99
+ export type BuildServerBundleEntry = {
100
+ readonly file: string;
101
+ readonly sizeBytes: number;
102
+ };
103
+
104
+ export type BuildServerBundleResult = {
105
+ readonly outDir: string;
106
+ readonly entries: readonly BuildServerBundleEntry[];
107
+ readonly runtimeDeps: Readonly<Record<string, string>>;
108
+ /** undefined wenn keine drizzle/migration-hooks.ts existiert. */
109
+ readonly migrationHooks: BuildServerBundleEntry | undefined;
110
+ };
111
+
112
+ export async function buildServerBundle(
113
+ options: BuildServerBundleOptions = {},
114
+ ): Promise<BuildServerBundleResult> {
115
+ if (!hasBun) {
116
+ throw new Error("buildServerBundle requires Bun runtime (Bun.build is missing).");
117
+ }
118
+
119
+ const cwd = options.cwd ?? process.cwd();
120
+ const outDir = resolve(cwd, options.outDir ?? "dist-server");
121
+
122
+ const serverEntry = discoverServerEntry(cwd);
123
+ if (!serverEntry) {
124
+ throw new Error(
125
+ `[buildServerBundle] Kein bin/main.ts in ${cwd}.\n Convention: bin/main.ts ist der Server-Bootstrap.`,
126
+ );
127
+ }
128
+
129
+ const repoRoot = findRepoRoot(cwd);
130
+ const kumikoCli = repoRoot ? join(repoRoot, "bin/kumiko.ts") : undefined;
131
+ if (kumikoCli && !existsSync(kumikoCli)) {
132
+ throw new Error(
133
+ `[buildServerBundle] Repo-Root erkannt (${repoRoot}), aber bin/kumiko.ts fehlt — kann Migrate-CLI nicht bündeln.`,
134
+ );
135
+ }
136
+ const migrationHooks = discoverMigrationHooks(cwd);
137
+ const drizzleConfig = discoverDrizzleConfig(cwd);
138
+
139
+ const externals = [
140
+ ...RUNTIME_EXTERNALS,
141
+ ...BUILD_ONLY_EXTERNALS,
142
+ ...(options.extraRuntimeExternals ?? []),
143
+ ...(options.extraBuildOnlyExternals ?? []),
144
+ ];
145
+
146
+ await rm(outDir, { recursive: true, force: true });
147
+ await mkdir(outDir, { recursive: true });
148
+
149
+ const entries: BuildServerBundleEntry[] = [];
150
+ entries.push(await bundleEntry(serverEntry, outDir, "server.js", externals));
151
+ if (kumikoCli) {
152
+ entries.push(await bundleEntry(kumikoCli, outDir, "kumiko.js", externals));
153
+ }
154
+ let migrationHooksEntry: BuildServerBundleEntry | undefined;
155
+ if (migrationHooks) {
156
+ migrationHooksEntry = await bundleEntry(
157
+ migrationHooks,
158
+ outDir,
159
+ "migration-hooks.js",
160
+ externals,
161
+ );
162
+ entries.push(migrationHooksEntry);
163
+ }
164
+ // drizzle.config.ts wird mit-bundelt damit drizzle-kit migrate im
165
+ // Runtime-Container den kumikoDrizzleConfig-Helper nicht via
166
+ // @cosmicdrift/kumiko-dev-server resolven muss (das Paket ist nicht installiert).
167
+ // Output behält die .ts-Endung — drizzle-kit's TS-Loader akzeptiert
168
+ // bundled JavaScript.
169
+ if (drizzleConfig) {
170
+ entries.push(await bundleEntry(drizzleConfig, outDir, "drizzle.config.ts", externals));
171
+ }
172
+
173
+ const runtimeDeps = await resolveRuntimeDepsVersions(repoRoot, [
174
+ ...RUNTIME_EXTERNALS,
175
+ ...(options.extraRuntimeExternals ?? []),
176
+ ]);
177
+
178
+ const runtimePkg = {
179
+ name: derivePkgName(cwd),
180
+ private: true,
181
+ type: "module",
182
+ scripts: { start: "bun run server.js" },
183
+ dependencies: runtimeDeps,
184
+ };
185
+ await writeFile(join(outDir, "package.json"), `${JSON.stringify(runtimePkg, null, 2)}\n`);
186
+
187
+ return { outDir, entries, runtimeDeps, migrationHooks: migrationHooksEntry };
188
+ }
189
+
190
+ async function bundleEntry(
191
+ entry: string,
192
+ outDir: string,
193
+ naming: string,
194
+ externals: readonly string[],
195
+ ): Promise<BuildServerBundleEntry> {
196
+ const result = await Bun.build({
197
+ entrypoints: [entry],
198
+ outdir: outDir,
199
+ target: "bun",
200
+ external: externals as string[],
201
+ naming,
202
+ minify: false,
203
+ });
204
+ if (!result.success) {
205
+ const logs = result.logs.map(String).join("\n");
206
+ throw new Error(`[buildServerBundle] Bundle ${naming} FAILED:\n${logs}`);
207
+ }
208
+ const out = result.outputs[0];
209
+ if (!out) {
210
+ throw new Error(`[buildServerBundle] Bundle ${naming}: no output produced.`);
211
+ }
212
+ return { file: naming, sizeBytes: out.size };
213
+ }
214
+
215
+ export function discoverServerEntry(cwd: string): string | undefined {
216
+ const tsEntry = join(cwd, "bin/main.ts");
217
+ if (existsSync(tsEntry)) return tsEntry;
218
+ const jsEntry = join(cwd, "bin/main.js");
219
+ if (existsSync(jsEntry)) return jsEntry;
220
+ return undefined;
221
+ }
222
+
223
+ export function discoverDrizzleConfig(cwd: string): string | undefined {
224
+ const path = join(cwd, "drizzle.config.ts");
225
+ return existsSync(path) ? path : undefined;
226
+ }
227
+
228
+ export function discoverMigrationHooks(cwd: string): string | undefined {
229
+ const path = join(cwd, "drizzle/migration-hooks.ts");
230
+ return existsSync(path) ? path : undefined;
231
+ }
232
+
233
+ // Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` und einer
234
+ // monorepo-package.json (workspaces[]) gefunden wird. Failsafe: nach 8
235
+ // Levels aufgeben — nichts gefunden, kein Repo-Root, dann liefert die
236
+ // Funktion undefined und der Caller skippt das CLI-Bundle.
237
+ function findRepoRoot(start: string): string | undefined {
238
+ let cur = start;
239
+ for (let i = 0; i < 8; i++) {
240
+ if (existsSync(join(cur, "bin/kumiko.ts"))) return cur;
241
+ const parent = dirname(cur);
242
+ if (parent === cur) return undefined;
243
+ cur = parent;
244
+ }
245
+ return undefined;
246
+ }
247
+
248
+ // Versionen für RUNTIME_EXTERNALS auflösen: erst aus framework + bundled-
249
+ // features package.json (Workspace-pinning), Fallback auf "*" wenn nichts
250
+ // gefunden. Ein Repo-Root muss existieren — sonst kann eh kein CLI-Bundle
251
+ // gemacht werden, also wird die Funktion gar nicht erst gerufen.
252
+ async function resolveRuntimeDepsVersions(
253
+ repoRoot: string | undefined,
254
+ packages: readonly string[],
255
+ ): Promise<Record<string, string>> {
256
+ const out: Record<string, string> = {};
257
+ if (!repoRoot) {
258
+ for (const pkg of packages) out[pkg] = "*";
259
+ return out;
260
+ }
261
+
262
+ const pinSources = [
263
+ join(repoRoot, "packages/framework/package.json"),
264
+ join(repoRoot, "packages/bundled-features/package.json"),
265
+ ];
266
+ const allDeps: Record<string, string> = {};
267
+ for (const path of pinSources) {
268
+ if (!existsSync(path)) continue;
269
+ const raw = await readFile(path, "utf-8");
270
+ const parsed = JSON.parse(raw) as { dependencies?: Record<string, string> };
271
+ Object.assign(allDeps, parsed.dependencies ?? {});
272
+ }
273
+ for (const pkg of packages) {
274
+ out[pkg] = allDeps[pkg] ?? "*";
275
+ }
276
+ return out;
277
+ }
278
+
279
+ function derivePkgName(cwd: string): string {
280
+ const pkgJson = join(cwd, "package.json");
281
+ if (!existsSync(pkgJson)) return "kumiko-app-runtime";
282
+ try {
283
+ const parsed = JSON.parse(readFileSync(pkgJson, "utf-8")) as { name?: string };
284
+ return parsed.name ? `${parsed.name}-runtime` : "kumiko-app-runtime";
285
+ } catch {
286
+ return "kumiko-app-runtime";
287
+ }
288
+ }
289
+
290
+ export function formatServerBuildResult(
291
+ result: BuildServerBundleResult,
292
+ durationMs: number,
293
+ ): string {
294
+ const cyan = "\x1b[36m";
295
+ const dim = "\x1b[2m";
296
+ const green = "\x1b[32m";
297
+ const reset = "\x1b[0m";
298
+ const lines: string[] = [];
299
+ lines.push("");
300
+ lines.push(` ${green}✓${reset} server bundle ${dim}(${durationMs}ms)${reset}`);
301
+ for (const entry of result.entries) {
302
+ const sizeMb = (entry.sizeBytes / 1024 / 1024).toFixed(2);
303
+ lines.push(` ${cyan}→${reset} ${entry.file} ${sizeMb} MB`);
304
+ }
305
+ const depCount = Object.keys(result.runtimeDeps).length;
306
+ lines.push(` ${dim}runtime-deps: ${depCount} packages${reset}`);
307
+ return lines.join("\n");
308
+ }
package/src/build.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Build-Toolchain Public-API. Eigener Sub-Path-Export `@cosmicdrift/kumiko-dev-server/build`
2
+ // damit der Main-Barrel kein Bun-Toolchain-Bundle in Production-Reads zieht
3
+ // (z.B. wenn drizzle-kit die App-Config unter Node lädt).
4
+
5
+ export {
6
+ ASSETS_DIR,
7
+ type BuildManifest,
8
+ type BuildProdBundleOptions,
9
+ type BuildResult,
10
+ buildProdBundle,
11
+ type ClientEntry,
12
+ discoverClientEntries,
13
+ discoverClientEntry,
14
+ discoverHtmlTemplate,
15
+ formatBuildResult,
16
+ injectAssetTags,
17
+ } from "./build-prod-bundle";
18
+
19
+ export {
20
+ type BuildServerBundleEntry,
21
+ type BuildServerBundleOptions,
22
+ type BuildServerBundleResult,
23
+ buildServerBundle,
24
+ discoverDrizzleConfig,
25
+ discoverMigrationHooks,
26
+ discoverServerEntry,
27
+ formatServerBuildResult,
28
+ } from "./build-server-bundle";