@cosmicdrift/kumiko-dev-server 0.19.1 → 0.20.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { buildServerBundle } from "../build-server-bundle";
6
+
7
+ // Baut ein Mini-App-Fixture (bin/main.ts + bin/kumiko.ts teilen ein Modul) und
8
+ // prüft das Variante-B-Verhalten: ein Bun.build-Call → server.js + kumiko.js als
9
+ // Entries + geteilter chunk statt zwei vollen Bundles.
10
+ function makeFixture(): string {
11
+ const dir = mkdtempSync(join(tmpdir(), "server-bundle-"));
12
+ mkdirSync(join(dir, "bin"));
13
+ mkdirSync(join(dir, "src"));
14
+ writeFileSync(join(dir, "package.json"), `${JSON.stringify({ name: "fixture-app" })}\n`);
15
+ writeFileSync(join(dir, "src/shared.ts"), "export function shared(): number { return 42; }\n");
16
+ // main.ts → wird zu server.js umbenannt; kumiko.ts macht findRepoRoot zum
17
+ // repoRoot dieses Fixtures, also wird kumiko.js gebaut.
18
+ writeFileSync(
19
+ join(dir, "bin/main.ts"),
20
+ `import { shared } from "../src/shared";\nconsole.log("server", shared());\n`,
21
+ );
22
+ writeFileSync(
23
+ join(dir, "bin/kumiko.ts"),
24
+ `import { shared } from "../src/shared";\nconsole.log("migrate", shared());\n`,
25
+ );
26
+ return dir;
27
+ }
28
+
29
+ describe("buildServerBundle (multi-entry + splitting)", () => {
30
+ test("produces server.js + kumiko.js as entries + a shared chunk", async () => {
31
+ const dir = makeFixture();
32
+ try {
33
+ const result = await buildServerBundle({ cwd: dir, outDir: join(dir, "dist-server") });
34
+ const outDir = result.outDir;
35
+
36
+ // main.ts wurde zu server.js umbenannt.
37
+ expect(existsSync(join(outDir, "server.js"))).toBe(true);
38
+ expect(existsSync(join(outDir, "main.js"))).toBe(false);
39
+ expect(existsSync(join(outDir, "kumiko.js"))).toBe(true);
40
+
41
+ const entryFiles = result.entries.map((e) => e.file).sort();
42
+ expect(entryFiles).toEqual(["kumiko.js", "server.js"]);
43
+
44
+ // Das geteilte Modul liegt in einem chunk, nicht in beiden Entries inlined.
45
+ expect(result.chunks.length).toBeGreaterThanOrEqual(1);
46
+ const serverSrc = readFileSync(join(outDir, "server.js"), "utf8");
47
+ expect(serverSrc).toContain("chunk-");
48
+
49
+ // Kein Legacy-Drizzle-Output mehr.
50
+ expect(existsSync(join(outDir, "migration-hooks.js"))).toBe(false);
51
+ expect(existsSync(join(outDir, "drizzle.config.ts"))).toBe(false);
52
+
53
+ // Runtime-package.json mit start-Script, ohne drizzle-deps.
54
+ const pkg = JSON.parse(readFileSync(join(outDir, "package.json"), "utf8"));
55
+ expect(pkg.scripts.start).toBe("bun run server.js");
56
+ expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-kit");
57
+ expect(Object.keys(pkg.dependencies)).not.toContain("drizzle-orm");
58
+
59
+ expect(result.totalBytes).toBeGreaterThan(0);
60
+ } finally {
61
+ rmSync(dir, { recursive: true, force: true });
62
+ }
63
+ });
64
+ });
@@ -1,37 +1,27 @@
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.
1
+ // buildServerBundle — Production-Server-Bundle für Kumiko-Apps. Pendant zu
2
+ // buildProdBundle (Client). Convention-Driven Discovery liest bin/main.ts
3
+ // (+ optional <repo>/bin/kumiko.ts) und produziert ein runtime-self-contained
4
+ // dist-server/ aus dem ein Bun-Alpine-Container ohne Monorepo bootet.
6
5
  //
7
6
  // Convention:
8
7
  //
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
8
+ // bin/main.ts → dist-server/server.js (App-Boot, ruft runProdApp)
9
+ // <repo>/bin/kumiko.ts → dist-server/kumiko.js (Migrate-CLI: `schema apply`,
10
+ // gefunden via walk-up bis bin/kumiko.ts)
11
+ // dist-server/package.json runtime-deps mit gepinnten Versionen
12
+ //
13
+ // Beide Entries werden in EINEM Bun.build-Call mit `splitting` gebaut: das
14
+ // Framework landet als geteilte chunk-*.js, server.js + kumiko.js sind schlanke
15
+ // Entries die sie importieren — statt das Framework pro Entry neu zu inlinen
16
+ // (vorher ~14 MB × N separate Bundles).
26
17
  //
27
18
  // Output:
28
19
  //
29
20
  // 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
21
+ // server.js ← App-Boot-Entry
22
+ // kumiko.js ← Migrate-CLI-Entry (wenn bin/kumiko.ts gefunden)
23
+ // chunk-*.js geteilte Framework-Chunks
24
+ // package.json runtime-deps mit gepinnten Versionen
35
25
  //
36
26
  // Externals — Pakete die NICHT ins Bundle gebakt werden, sondern via
37
27
  // `bun install` im Runtime-Container nachgeholt werden:
@@ -41,52 +31,34 @@
41
31
  // dist-server/package.json#dependencies.
42
32
  //
43
33
  // 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.
34
+ // App-Code aber nie. Tree-Shake wirft sie aus dem
35
+ // Bundle, der external-Marker schaltet nur das
36
+ // resolution-during-build ab. NICHT in runtime-deps.
53
37
 
54
- import { existsSync, readFileSync } from "node:fs";
55
- import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
38
+ import { existsSync, readFileSync, statSync } from "node:fs";
39
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
56
40
  import { dirname, join, resolve } from "node:path";
57
41
 
58
42
  const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
59
43
 
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".
44
+ // Pakete die das Bundle zur Laufzeit weiter referenziert (`from "<pkg>"` nach
45
+ // Tree-Shake). Native bindings (argon2), worker-thread-Loader (bullmq,
46
+ // ioredis), dynamic-require (postgres, temporal-polyfill) — alles was unter
47
+ // Bun-bundling bricht. (drizzle-kit/drizzle-orm sind raus: der Migrate-Pfad
48
+ // nutzt jetzt den framework-eigenen runMigrationsFromDir, kein drizzle-kit.)
70
49
  const RUNTIME_EXTERNALS = [
71
50
  "@node-rs/argon2",
72
51
  "bullmq",
73
- "drizzle-kit",
74
- "drizzle-orm",
75
52
  "ioredis",
76
53
  "postgres",
77
54
  "temporal-polyfill",
78
55
  ] as const;
79
56
 
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
- //
86
- // drizzle-kit's dialect-resolver macht dynamic-imports zu allen DB-driver-
87
- // packages (planetscale/libsql/sqlite/neon/vercel/mysql2). Wir nutzen nur
88
- // postgres → diese werden never-loaded zur Runtime, aber der Bundler will
89
- // sie resolven. Aufgedeckt durch C1 Empfehlung 4 (bundle-smoke).
57
+ // Pakete die nur im Build-Stack erscheinen (transitive Imports im Framework),
58
+ // aber vom App-Code nicht referenziert werden. Ohne external-Markierung
59
+ // scheitert bun build an dynamic-imports. Tree-Shake wirft sie eh aus dem
60
+ // Bundle der Marker schaltet nur das resolution-during-build ab. NICHT in
61
+ // runtime-deps.
90
62
  const BUILD_ONLY_EXTERNALS = [
91
63
  "meilisearch",
92
64
  "pino",
@@ -121,12 +93,20 @@ export type BuildServerBundleEntry = {
121
93
 
122
94
  export type BuildServerBundleResult = {
123
95
  readonly outDir: string;
96
+ /** Benannte Entry-Files (server.js, ggf. kumiko.js). */
124
97
  readonly entries: readonly BuildServerBundleEntry[];
98
+ /** Geteilte Framework-Chunks (von splitting). */
99
+ readonly chunks: readonly BuildServerBundleEntry[];
100
+ /** Gesamt-Größe aller Outputs (entries + chunks) in Bytes. */
101
+ readonly totalBytes: number;
125
102
  readonly runtimeDeps: Readonly<Record<string, string>>;
126
- /** undefined wenn keine drizzle/migration-hooks.ts existiert. */
127
- readonly migrationHooks: BuildServerBundleEntry | undefined;
128
103
  };
129
104
 
105
+ // Bun benennt Entries nach Source-Basename (bin/main.ts → main.js). server.js
106
+ // ist die Runtime-Convention; der Entry wird von nichts importiert (nur die
107
+ // chunks werden referenziert, per relativem Pfad), darum ist der Rename sicher.
108
+ const ENTRY_RENAMES: Readonly<Record<string, string>> = { "main.js": "server.js" };
109
+
130
110
  export async function buildServerBundle(
131
111
  options: BuildServerBundleOptions = {},
132
112
  ): Promise<BuildServerBundleResult> {
@@ -151,8 +131,6 @@ export async function buildServerBundle(
151
131
  `[buildServerBundle] Repo-Root erkannt (${repoRoot}), aber bin/kumiko.ts fehlt — kann Migrate-CLI nicht bündeln.`,
152
132
  );
153
133
  }
154
- const migrationHooks = discoverMigrationHooks(cwd);
155
- const drizzleConfig = discoverDrizzleConfig(cwd);
156
134
 
157
135
  const externals = [
158
136
  ...RUNTIME_EXTERNALS,
@@ -164,29 +142,42 @@ export async function buildServerBundle(
164
142
  await rm(outDir, { recursive: true, force: true });
165
143
  await mkdir(outDir, { recursive: true });
166
144
 
167
- const entries: BuildServerBundleEntry[] = [];
168
- entries.push(await bundleEntry(serverEntry, outDir, "server.js", externals));
169
- if (kumikoCli) {
170
- entries.push(await bundleEntry(kumikoCli, outDir, "kumiko.js", externals));
171
- }
172
- let migrationHooksEntry: BuildServerBundleEntry | undefined;
173
- if (migrationHooks) {
174
- migrationHooksEntry = await bundleEntry(
175
- migrationHooks,
176
- outDir,
177
- "migration-hooks.js",
178
- externals,
179
- );
180
- entries.push(migrationHooksEntry);
145
+ // Ein Bun.build-Call mit allen Entries + splitting → das Framework wird
146
+ // einmal als shared chunk abgelegt statt pro Entry inlined.
147
+ const entrypoints = kumikoCli ? [serverEntry, kumikoCli] : [serverEntry];
148
+ const result = await Bun.build({
149
+ entrypoints,
150
+ outdir: outDir,
151
+ target: "bun",
152
+ external: externals as string[],
153
+ splitting: true,
154
+ sourcemap: "none",
155
+ naming: { entry: "[name].js", chunk: "chunk-[hash].js" },
156
+ minify: false,
157
+ });
158
+ if (!result.success) {
159
+ const logs = result.logs.map(String).join("\n");
160
+ throw new Error(`[buildServerBundle] build FAILED:\n${logs}`);
181
161
  }
182
- // drizzle.config.ts wird mit-bundelt damit drizzle-kit migrate im
183
- // Runtime-Container den kumikoDrizzleConfig-Helper nicht via
184
- // @cosmicdrift/kumiko-dev-server resolven muss (das Paket ist nicht installiert).
185
- // Output behält die .ts-Endung — drizzle-kit's TS-Loader akzeptiert
186
- // bundled JavaScript.
187
- if (drizzleConfig) {
188
- entries.push(await bundleEntry(drizzleConfig, outDir, "drizzle.config.ts", externals));
162
+
163
+ // Größe von Disk lesen, nicht aus out.size — letzteres ist für gesplittete
164
+ // Entry-Points unzuverlässig (meldet teils ~0 für den Entry-Stub).
165
+ const entries: BuildServerBundleEntry[] = [];
166
+ const chunks: BuildServerBundleEntry[] = [];
167
+ for (const out of result.outputs) {
168
+ const base = out.path.split("/").pop() ?? out.path;
169
+ if (out.kind === "entry-point") {
170
+ const desired = ENTRY_RENAMES[base] ?? base;
171
+ if (desired !== base) {
172
+ await rename(join(outDir, base), join(outDir, desired));
173
+ }
174
+ entries.push({ file: desired, sizeBytes: statSync(join(outDir, desired)).size });
175
+ } else if (out.kind === "chunk") {
176
+ chunks.push({ file: base, sizeBytes: statSync(join(outDir, base)).size });
177
+ }
189
178
  }
179
+ entries.sort((a, b) => a.file.localeCompare(b.file));
180
+ const totalBytes = [...entries, ...chunks].reduce((sum, e) => sum + e.sizeBytes, 0);
190
181
 
191
182
  const runtimeDeps = await resolveRuntimeDepsVersions(repoRoot, [
192
183
  ...RUNTIME_EXTERNALS,
@@ -202,32 +193,7 @@ export async function buildServerBundle(
202
193
  };
203
194
  await writeFile(join(outDir, "package.json"), `${JSON.stringify(runtimePkg, null, 2)}\n`);
204
195
 
205
- return { outDir, entries, runtimeDeps, migrationHooks: migrationHooksEntry };
206
- }
207
-
208
- async function bundleEntry(
209
- entry: string,
210
- outDir: string,
211
- naming: string,
212
- externals: readonly string[],
213
- ): Promise<BuildServerBundleEntry> {
214
- const result = await Bun.build({
215
- entrypoints: [entry],
216
- outdir: outDir,
217
- target: "bun",
218
- external: externals as string[],
219
- naming,
220
- minify: false,
221
- });
222
- if (!result.success) {
223
- const logs = result.logs.map(String).join("\n");
224
- throw new Error(`[buildServerBundle] Bundle ${naming} FAILED:\n${logs}`);
225
- }
226
- const out = result.outputs[0];
227
- if (!out) {
228
- throw new Error(`[buildServerBundle] Bundle ${naming}: no output produced.`);
229
- }
230
- return { file: naming, sizeBytes: out.size };
196
+ return { outDir, entries, chunks, totalBytes, runtimeDeps };
231
197
  }
232
198
 
233
199
  export function discoverServerEntry(cwd: string): string | undefined {
@@ -238,20 +204,9 @@ export function discoverServerEntry(cwd: string): string | undefined {
238
204
  return undefined;
239
205
  }
240
206
 
241
- export function discoverDrizzleConfig(cwd: string): string | undefined {
242
- const path = join(cwd, "drizzle.config.ts");
243
- return existsSync(path) ? path : undefined;
244
- }
245
-
246
- export function discoverMigrationHooks(cwd: string): string | undefined {
247
- const path = join(cwd, "drizzle/migration-hooks.ts");
248
- return existsSync(path) ? path : undefined;
249
- }
250
-
251
- // Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` und einer
252
- // monorepo-package.json (workspaces[]) gefunden wird. Failsafe: nach 8
253
- // Levels aufgeben — nichts gefunden, kein Repo-Root, dann liefert die
254
- // Funktion undefined und der Caller skippt das CLI-Bundle.
207
+ // Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` gefunden wird.
208
+ // Failsafe: nach 8 Levels aufgeben — nichts gefunden, kein Repo-Root, dann
209
+ // liefert die Funktion undefined und der Caller skippt das CLI-Bundle.
255
210
  function findRepoRoot(start: string): string | undefined {
256
211
  let cur = start;
257
212
  for (let i = 0; i < 8; i++) {
@@ -313,14 +268,22 @@ export function formatServerBuildResult(
313
268
  const dim = "\x1b[2m";
314
269
  const green = "\x1b[32m";
315
270
  const reset = "\x1b[0m";
271
+ const mb = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2);
316
272
  const lines: string[] = [];
317
273
  lines.push("");
318
274
  lines.push(` ${green}✓${reset} server bundle ${dim}(${durationMs}ms)${reset}`);
319
275
  for (const entry of result.entries) {
320
- const sizeMb = (entry.sizeBytes / 1024 / 1024).toFixed(2);
321
- lines.push(` ${cyan}→${reset} ${entry.file} ${sizeMb} MB`);
276
+ lines.push(` ${cyan}→${reset} ${entry.file} ${mb(entry.sizeBytes)} MB`);
277
+ }
278
+ if (result.chunks.length > 0) {
279
+ const chunkBytes = result.chunks.reduce((sum, c) => sum + c.sizeBytes, 0);
280
+ lines.push(
281
+ ` ${cyan}→${reset} ${result.chunks.length} shared chunk(s) ${mb(chunkBytes)} MB`,
282
+ );
322
283
  }
323
284
  const depCount = Object.keys(result.runtimeDeps).length;
324
- lines.push(` ${dim}runtime-deps: ${depCount} packages${reset}`);
285
+ lines.push(
286
+ ` ${dim}total: ${mb(result.totalBytes)} MB · runtime-deps: ${depCount} packages${reset}`,
287
+ );
325
288
  return lines.join("\n");
326
289
  }
package/src/build.ts CHANGED
@@ -21,8 +21,6 @@ export {
21
21
  type BuildServerBundleOptions,
22
22
  type BuildServerBundleResult,
23
23
  buildServerBundle,
24
- discoverDrizzleConfig,
25
- discoverMigrationHooks,
26
24
  discoverServerEntry,
27
25
  formatServerBuildResult,
28
26
  } from "./build-server-bundle";