@cosmicdrift/kumiko-dev-server 0.307.0 → 0.309.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.307.0",
3
+ "version": "0.309.0",
4
4
  "description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -59,10 +59,10 @@
59
59
  "kumiko-upgrade": "./bin/kumiko-upgrade.ts"
60
60
  },
61
61
  "dependencies": {
62
- "@cosmicdrift/kumiko-bundled-features": "0.307.0",
63
- "@cosmicdrift/kumiko-framework": "0.307.0",
64
- "@cosmicdrift/kumiko-headless": "0.307.0",
65
- "@cosmicdrift/kumiko-server-runtime": "0.307.0",
62
+ "@cosmicdrift/kumiko-bundled-features": "0.309.0",
63
+ "@cosmicdrift/kumiko-framework": "0.309.0",
64
+ "@cosmicdrift/kumiko-headless": "0.309.0",
65
+ "@cosmicdrift/kumiko-server-runtime": "0.309.0",
66
66
  "ts-morph": "^28.0.0"
67
67
  },
68
68
  "publishConfig": {
@@ -17,7 +17,7 @@ import {
17
17
  TestUsers,
18
18
  unsafePushTables,
19
19
  } from "@cosmicdrift/kumiko-framework/stack";
20
- import { z } from "zod";
20
+ import * as z from "zod";
21
21
  import { mergeConfigResolverDefault } from "../run-dev-app";
22
22
 
23
23
  // Pins runDevApp's ENV→config-app-override wiring: mergeConfigResolverDefault
@@ -16,10 +16,11 @@ import {
16
16
  type TenantId,
17
17
  } from "@cosmicdrift/kumiko-framework/engine";
18
18
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
19
- import { z } from "zod";
19
+ import * as z from "zod";
20
20
  import {
21
21
  createKumikoServer,
22
22
  type KumikoServerHandle,
23
+ PROD_BUNDLES_ENV,
23
24
  STYLESHEET_WATCH_ENV,
24
25
  } from "../create-kumiko-server";
25
26
 
@@ -540,6 +541,10 @@ describe("createKumikoServer — real Bun.build (buildClient)", () => {
540
541
  expect(body.length).toBeGreaterThan(0);
541
542
  expect(body).toMatch(/ping/);
542
543
 
544
+ const mapRes = await handle.fetch(new Request("http://localhost/client.js.map"));
545
+ expect(mapRes.status).toBe(200);
546
+ expect(await mapRes.text()).toMatch(/"sources"/);
547
+
543
548
  // Stop the watcher before teardown rmSync deletes clientEntry out from
544
549
  // under it — a bare "client.tsx" delete event classifies as "restart"
545
550
  // (classifyChange only special-cases endsWith("/client.tsx")) and
@@ -550,6 +555,62 @@ describe("createKumikoServer — real Bun.build (buildClient)", () => {
550
555
  rmSync(tmpDir, { recursive: true, force: true });
551
556
  }
552
557
  });
558
+
559
+ test("PROD_BUNDLES_ENV builds a prod-shaped bundle: split chunk served, no sourcemap, NODE_ENV replaced", async () => {
560
+ const tmpDir = mkdtempSync(join(tmpdir(), "kumiko-real-build-prod-"));
561
+ const entry = join(tmpDir, "client.tsx");
562
+ writeFileSync(
563
+ entry,
564
+ `export async function loadLazy() {
565
+ const m = await import("./lazy.ts");
566
+ return m.hello();
567
+ }
568
+ `,
569
+ );
570
+ writeFileSync(
571
+ join(tmpDir, "lazy.ts"),
572
+ `export function hello() {
573
+ return "env=" + process.env.NODE_ENV;
574
+ }
575
+ `,
576
+ );
577
+ const previousProdBundles = process.env[PROD_BUNDLES_ENV];
578
+ process.env[PROD_BUNDLES_ENV] = "1";
579
+ try {
580
+ handle = await createKumikoServer({
581
+ features: [probeFeature],
582
+ port: 0,
583
+ installSignalHandlers: false,
584
+ clientEntry: entry,
585
+ stylesheet: false,
586
+ });
587
+ const res = await handle.fetch(new Request("http://localhost/client.js"));
588
+ expect(res.status).toBe(200);
589
+ const body = await res.text();
590
+
591
+ // Only the dynamic import() call points at the lazy chunk — the
592
+ // entry also statically imports a shared async-loader chunk.
593
+ const chunkMatch = body.match(/import\("(?:\.\/)?(chunk-[\w-]+\.js)"\)/);
594
+ expect(chunkMatch).not.toBeNull();
595
+ const normalizedChunkPath = `/${chunkMatch ? (chunkMatch[1] ?? "") : ""}`;
596
+
597
+ const chunkRes = await handle.fetch(new Request(`http://localhost${normalizedChunkPath}`));
598
+ expect(chunkRes.status).toBe(200);
599
+ expect(chunkRes.headers.get("content-type")).toMatch(/application\/javascript/);
600
+ const chunkBody = await chunkRes.text();
601
+ expect(chunkBody).toContain("env=production");
602
+
603
+ const mapRes = await handle.fetch(new Request("http://localhost/client.js.map"));
604
+ expect(mapRes.status).toBe(404);
605
+
606
+ await handle.stop();
607
+ handle = undefined;
608
+ } finally {
609
+ if (previousProdBundles === undefined) delete process.env[PROD_BUNDLES_ENV];
610
+ else process.env[PROD_BUNDLES_ENV] = previousProdBundles;
611
+ rmSync(tmpDir, { recursive: true, force: true });
612
+ }
613
+ });
553
614
  });
554
615
 
555
616
  describe("createKumikoServer — hot-reload broadcast", () => {
@@ -8,7 +8,7 @@ import { describe, expect, it } from "bun:test";
8
8
  import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { composeEnvSchema, KumikoBootError } from "@cosmicdrift/kumiko-framework/env";
10
10
  import { runProdApp } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
11
- import { z } from "zod";
11
+ import * as z from "zod";
12
12
  import { frameworkCoreEnvSchema } from "../env-schema";
13
13
  import * as devServerPublicApi from "../index";
14
14
 
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
  import { composeEnvSchema, KumikoBootError, parseEnv } from "@cosmicdrift/kumiko-framework/env";
3
- import { z } from "zod";
3
+ import * as z from "zod";
4
4
  import { type FrameworkCoreEnv, frameworkCoreEnvSchema } from "../env-schema";
5
5
 
6
6
  // Statt Sentinel-throw IM try (ein nicht-werfender parseEnv ließe den
@@ -23,7 +23,7 @@ import {
23
23
  } from "@cosmicdrift/kumiko-framework/engine";
24
24
  import { TestUsers } from "@cosmicdrift/kumiko-framework/stack";
25
25
  import * as jose from "jose";
26
- import { z } from "zod";
26
+ import * as z from "zod";
27
27
  import type { KumikoServerHandle } from "../create-kumiko-server";
28
28
  import { runDevApp } from "../run-dev-app";
29
29
 
package/src/changes.json CHANGED
@@ -1,4 +1,9 @@
1
1
  [
2
+ {
3
+ "version": "0.308.0",
4
+ "type": "improvement",
5
+ "title": "Dev server can build prod-shaped client bundles, E2E uses them"
6
+ },
2
7
  {
3
8
  "version": "0.307.0",
4
9
  "type": "fix",
@@ -269,6 +269,10 @@ const AUTH_COOKIE = "kumiko_auth";
269
269
  // runs under Node and cannot import this module.
270
270
  export const STYLESHEET_WATCH_ENV = "KUMIKO_DEV_STYLESHEET_WATCH";
271
271
 
272
+ // Duplicated in packages/testing/src/e2e/constants.ts: the Playwright config
273
+ // runs under Node and cannot import this module.
274
+ export const PROD_BUNDLES_ENV = "KUMIKO_DEV_PROD_BUNDLES";
275
+
272
276
  // Reload snippet injected into every page-load so the browser
273
277
  // subscribes to /_reload without the HTML needing to hard-code it.
274
278
  //
@@ -325,7 +329,13 @@ const DEFAULT_HTML = `<!doctype html>
325
329
  </html>
326
330
  `;
327
331
 
328
- type ClientBundle = { readonly js: string; readonly map: string };
332
+ type ClientBundle = {
333
+ readonly js: string;
334
+ readonly map: string;
335
+ // Only set when PROD_BUNDLES_ENV builds with splitting. Path -> content,
336
+ // e.g. "/chunk-abc123.js" -> "...".
337
+ readonly chunks?: ReadonlyMap<string, string>;
338
+ };
329
339
 
330
340
  async function buildClient(entry: string): Promise<ClientBundle> {
331
341
  if (!hasBun) {
@@ -334,23 +344,35 @@ async function buildClient(entry: string): Promise<ClientBundle> {
334
344
  );
335
345
  }
336
346
  const unminified = process.env["KUMIKO_DEV_UNMINIFIED"] === "1";
347
+ // Same splitting/sourcemap/NODE_ENV as build-prod-bundle.ts, opt-in via
348
+ // env so `bun dev` stays unchanged.
349
+ const prodShaped = process.env[PROD_BUNDLES_ENV] === "1";
337
350
  const built = await Bun.build({
338
351
  entrypoints: [entry],
339
352
  target: "browser",
340
353
  minify: !unminified,
341
- sourcemap: "linked",
354
+ sourcemap: prodShaped ? "none" : "linked",
355
+ splitting: prodShaped,
356
+ ...(prodShaped ? { define: { "process.env.NODE_ENV": JSON.stringify("production") } } : {}),
342
357
  });
343
358
  if (!built.success) {
344
359
  logError("[kumiko-server] client bundle failed:");
345
360
  for (const log of built.logs) logError(log);
346
361
  throw new Error("client bundle failed");
347
362
  }
348
- const jsOutput = built.outputs.find((o) => o.path.endsWith(".js"));
363
+ const jsOutput = built.outputs.find((o) => o.kind === "entry-point");
349
364
  const mapOutput = built.outputs.find((o) => o.path.endsWith(".js.map"));
350
365
  if (!jsOutput) throw new Error("[kumiko-server] client bundle produced no .js output");
366
+ const chunks = new Map<string, string>();
367
+ for (const output of built.outputs) {
368
+ if (output.kind !== "chunk") continue;
369
+ const chunkPath = `/${output.path.replace(/^\.\//, "")}`;
370
+ chunks.set(chunkPath, await output.text());
371
+ }
351
372
  return {
352
373
  js: await jsOutput.text(),
353
374
  map: mapOutput ? await mapOutput.text() : "",
375
+ chunks,
354
376
  };
355
377
  }
356
378
 
@@ -1055,6 +1077,17 @@ export async function createKumikoServer(
1055
1077
  headers: { "Content-Type": "application/javascript; charset=utf-8" },
1056
1078
  });
1057
1079
  }
1080
+ // Looked up in the live bundles instead of a separate index, which a
1081
+ // hot-reload rebuild would leave stale.
1082
+ for (const bundle of clientBundles.values()) {
1083
+ const chunk = bundle.chunks?.get(url.pathname);
1084
+ if (chunk !== undefined) {
1085
+ return new Response(chunk, {
1086
+ headers: { "Content-Type": "application/javascript; charset=utf-8" },
1087
+ });
1088
+ }
1089
+ }
1090
+
1058
1091
  // .js.map-Variante: gleicher Lookup mit /.map abgeschnitten.
1059
1092
  if (url.pathname.endsWith(".js.map")) {
1060
1093
  const jsPath = url.pathname.slice(0, -".map".length);
package/src/env-schema.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  // Feature-specific vars (JWT_SECRET, KUMIKO_SECRETS_MASTER_KEY_*) live
16
16
  // in their owning feature's envSchema — Phase 2 (bundled-features).
17
17
 
18
- import { z } from "zod";
18
+ import * as z from "zod";
19
19
 
20
20
  /** Env-vars read by framework-core (api/server, db/connection,
21
21
  * dev-server/run-prod-app). NOT including feature-specific vars.
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // - runDevApp (high-level)
11
11
  // Mischt die Standard-Features (config/user/tenant/auth-email-
12
12
  // password) automatisch dazu wenn `auth` gesetzt ist, wired die
13
- // Login-Routes + Error-Map, ruft seedAdmin im onAfterSetup. Default
13
+ // Login-Routes + Error-Map, ruft seedAdminGuarded im onAfterSetup. Default
14
14
  // für Sample-Apps und Showcases — 5-10 Zeilen Bootstrap statt 50.
15
15
 
16
16
  export {
@@ -54,6 +54,7 @@ export {
54
54
  type CreateKumikoServerOptions,
55
55
  createKumikoServer,
56
56
  type KumikoServerHandle,
57
+ PROD_BUNDLES_ENV,
57
58
  resolveStylesheet,
58
59
  STYLESHEET_WATCH_ENV,
59
60
  } from "./create-kumiko-server";
@@ -1,8 +1,7 @@
1
- // runDevApp — high-level dev-server wrapper für Sample-Apps und
2
- // Showcases. Mischt die Standard-Features (config/user/tenant/auth-
3
- // email-password) automatisch dazu wenn auth-mode aktiv ist, wired das
4
- // AuthRoutesConfig + Login-Error-Map, und ruft seedAdmin() im
5
- // onAfterSetup. Reduziert den Sample-Bootstrap von 50 Zeilen auf 5-10.
1
+ // High-level dev-server wrapper for sample apps and showcases. In auth mode it
2
+ // mixes in the standard features (config/user/tenant/auth-email-password),
3
+ // wires AuthRoutesConfig plus the login error map and calls seedAdminGuarded()
4
+ // in onAfterSetup, cutting a sample bootstrap from ~50 lines to 5-10.
6
5
  //
7
6
  // Auto-mix passiert NUR im auth-mode. Ohne `auth`-Block bleibt der
8
7
  // Server im Auto-Mint-JWT-Modus (Dev-Default) und mischt nichts dazu —
@@ -16,7 +15,7 @@
16
15
  import { AuthErrors, AuthHandlers } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
17
16
  import {
18
17
  type SeedAdminOptions,
19
- seedAdmin,
18
+ seedAdminGuarded,
20
19
  } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
21
20
  import {
22
21
  EXT_SESSION_STORE,
@@ -209,9 +208,8 @@ export type RunDevAppOptions = {
209
208
  /** SIGINT/SIGTERM-Handler installieren (Default true; in Tests auf
210
209
  * false damit repeated boots keine Listener akkumulieren). */
211
210
  readonly installSignalHandlers?: boolean;
212
- /** Auth-Mode: Standard-Features dazu, Auth-Routes wired, seedAdmin
213
- * läuft im onAfterSetup. Ohne `auth` läuft der Server im Auto-Mint-
214
- * JWT-Modus. */
211
+ /** Auth mode: standard features mixed in, auth routes wired, seedAdminGuarded
212
+ * runs in onAfterSetup. Without `auth` the server stays in auto-mint JWT mode. */
215
213
  readonly auth?: RunDevAppAuthOptions;
216
214
  /** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
217
215
  * Array-Reihenfolge. */
@@ -598,7 +596,7 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
598
596
  );
599
597
  }
600
598
  if (effectiveAuth) {
601
- await seedAdmin(stack.db, effectiveAuth.admin);
599
+ await seedAdminGuarded(stack.db, effectiveAuth.admin);
602
600
  }
603
601
  // Apply r.config({ seeds }) declared by any registered feature.
604
602
  // Runs before user-supplied seed callbacks so those can read /