@cosmicdrift/kumiko-dev-server 0.306.0 → 0.308.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 +5 -5
- package/src/__tests__/config-resolver-default.integration.test.ts +1 -1
- package/src/__tests__/create-kumiko-server.integration.test.ts +62 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/env-schema.test.ts +1 -1
- package/src/__tests__/run-dev-app.integration.test.ts +1 -1
- package/src/changes.json +10 -0
- package/src/create-kumiko-server.ts +55 -6
- package/src/env-schema.ts +1 -1
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.308.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.
|
|
63
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
64
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
65
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
62
|
+
"@cosmicdrift/kumiko-bundled-features": "0.308.0",
|
|
63
|
+
"@cosmicdrift/kumiko-framework": "0.308.0",
|
|
64
|
+
"@cosmicdrift/kumiko-headless": "0.308.0",
|
|
65
|
+
"@cosmicdrift/kumiko-server-runtime": "0.308.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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,14 @@
|
|
|
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
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"version": "0.307.0",
|
|
9
|
+
"type": "fix",
|
|
10
|
+
"title": "Dev-server job queues no longer collide across parallel boots"
|
|
11
|
+
},
|
|
2
12
|
{
|
|
3
13
|
"version": "0.305.0",
|
|
4
14
|
"type": "improvement",
|
|
@@ -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 = {
|
|
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.
|
|
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
|
|
|
@@ -849,6 +871,16 @@ export async function createKumikoServer(
|
|
|
849
871
|
const devDbName = process.env["KUMIKO_DEV_DB_NAME"];
|
|
850
872
|
const persistentDb = devDbName !== undefined && devDbName !== "";
|
|
851
873
|
|
|
874
|
+
// Redis is shared across boots AND across apps (unlike the per-boot test
|
|
875
|
+
// DB, which is ephemeral or persistent per KUMIKO_DEV_DB_NAME), so every
|
|
876
|
+
// boot needs its own queue-name prefix — otherwise two dev/e2e servers
|
|
877
|
+
// running in parallel drain each other's queues ("Unknown job: ..."). A
|
|
878
|
+
// persistent DB implies a stable prefix so jobs survive a restart; an
|
|
879
|
+
// ephemeral DB gets a fresh prefix per boot, matching the fresh DB.
|
|
880
|
+
const jobQueueNamePrefix = persistentDb
|
|
881
|
+
? `kumiko-dev-${devDbName}`
|
|
882
|
+
: `kumiko-dev-${crypto.randomUUID()}`;
|
|
883
|
+
|
|
852
884
|
logInfo(
|
|
853
885
|
`[kumiko-server] booting Kumiko stack${
|
|
854
886
|
persistentDb ? ` — persistent DB "${devDbName}"` : " — ephemeral test DB"
|
|
@@ -866,9 +898,11 @@ export async function createKumikoServer(
|
|
|
866
898
|
effectiveFeatures: options.effectiveFeatures,
|
|
867
899
|
}),
|
|
868
900
|
...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
|
|
869
|
-
// jobs
|
|
870
|
-
// consumer/cron-scheduler per lane, so runOnBoot/cron jobs
|
|
871
|
-
|
|
901
|
+
// jobs.consumerLane unset = enqueuer-only; startDevJobRunners below is
|
|
902
|
+
// the sole consumer/cron-scheduler per lane, so runOnBoot/cron jobs
|
|
903
|
+
// don't double-fire. queueNamePrefix keeps this boot's queues isolated
|
|
904
|
+
// from every other dev/e2e server sharing the same Redis.
|
|
905
|
+
jobs: { queueNamePrefix: jobQueueNamePrefix },
|
|
872
906
|
});
|
|
873
907
|
await createEventsTable(stack.db);
|
|
874
908
|
await pushEntityProjectionTables(stack, stack.registry);
|
|
@@ -921,6 +955,10 @@ export async function createKumikoServer(
|
|
|
921
955
|
// ctx.write/ctx.writeAs/ctx.queryAs inside a dev-run job throw on their
|
|
922
956
|
// first call (kumiko-framework#2553).
|
|
923
957
|
dispatcher: stack.dispatcher,
|
|
958
|
+
// Same isolation reasoning as setupTestStack's jobs.queueNamePrefix
|
|
959
|
+
// above — both must use the same prefix so enqueuer and consumer agree
|
|
960
|
+
// on queue names.
|
|
961
|
+
queueNamePrefix: jobQueueNamePrefix,
|
|
924
962
|
});
|
|
925
963
|
|
|
926
964
|
// Dev user = TestUsers.admin. Demo features are openToAll but the
|
|
@@ -1039,6 +1077,17 @@ export async function createKumikoServer(
|
|
|
1039
1077
|
headers: { "Content-Type": "application/javascript; charset=utf-8" },
|
|
1040
1078
|
});
|
|
1041
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
|
+
|
|
1042
1091
|
// .js.map-Variante: gleicher Lookup mit /.map abgeschnitten.
|
|
1043
1092
|
if (url.pathname.endsWith(".js.map")) {
|
|
1044
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
|
|
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.
|