@cosmicdrift/kumiko-dev-server 0.16.0 → 0.19.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/bin/kumiko-schema.ts +19 -0
- package/package.json +2 -1
- package/src/__tests__/classify-change.test.ts +1 -1
- package/src/__tests__/{create-kumiko-server.integration.ts → create-kumiko-server.integration.test.ts} +6 -5
- package/src/__tests__/resolve-tailwind-cli.test.ts +33 -1
- package/src/__tests__/{run-prod-app.integration.ts → run-prod-app.integration.test.ts} +9 -28
- package/src/build-prod-bundle.ts +25 -7
- package/src/create-kumiko-server.ts +14 -3
- package/src/resolve-tailwind-cli.ts +18 -1
- package/src/run-prod-app.ts +14 -11
- /package/src/__tests__/{build-prod-bundle.integration.ts → build-prod-bundle.integration.test.ts} +0 -0
- /package/src/__tests__/{compose-features-wiring.integration.ts → compose-features-wiring.integration.test.ts} +0 -0
- /package/src/__tests__/{config-seed-boot.integration.ts → config-seed-boot.integration.test.ts} +0 -0
- /package/src/__tests__/{env-schema.integration.ts → env-schema.integration.test.ts} +0 -0
- /package/src/__tests__/{walkthrough.integration.ts → walkthrough.integration.test.ts} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// Shipped, app-facing schema-migration CLI: `kumiko-schema generate|apply|baseline|status`.
|
|
3
|
+
//
|
|
4
|
+
// Self-contained — delegates to the framework's runSchemaCli core, so apps run
|
|
5
|
+
// migrations without the full dev `kumiko` CLI (which eager-loads ts-morph-heavy
|
|
6
|
+
// dev commands not shipped to apps). Run from the app workspace root:
|
|
7
|
+
//
|
|
8
|
+
// bunx kumiko-schema generate init
|
|
9
|
+
// bunx kumiko-schema apply
|
|
10
|
+
// bunx kumiko-schema baseline # adopt an existing DB (tables already exist)
|
|
11
|
+
// bunx kumiko-schema status
|
|
12
|
+
|
|
13
|
+
import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";
|
|
14
|
+
|
|
15
|
+
// biome-ignore lint/suspicious/noConsole: CLI output is the feature.
|
|
16
|
+
const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };
|
|
17
|
+
const appCwd = process.env["INIT_CWD"] ?? process.cwd();
|
|
18
|
+
const code = await runSchemaCli(process.argv.slice(2), appCwd, out);
|
|
19
|
+
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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>",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"bin": {
|
|
43
43
|
"kumiko-build": "./bin/kumiko-build.ts",
|
|
44
44
|
"kumiko-dev": "./bin/kumiko-dev.ts",
|
|
45
|
+
"kumiko-schema": "./bin/kumiko-schema.ts",
|
|
45
46
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
46
47
|
},
|
|
47
48
|
"dependencies": {
|
|
@@ -50,7 +50,7 @@ describe("classifyChange", () => {
|
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test("Integration-Test → ignore (würde sonst Schema-Restart auslösen)", () => {
|
|
53
|
-
expect(classifyChange("/abs/samples/foo/src/feature.integration.ts")).toBe("ignore");
|
|
53
|
+
expect(classifyChange("/abs/samples/foo/src/feature.integration.test.ts")).toBe("ignore");
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
test("E2E-Test → ignore", () => {
|
|
@@ -137,12 +137,13 @@ describe("createKumikoServer", () => {
|
|
|
137
137
|
expect(res.headers.get("content-type") ?? "").not.toMatch(/text\/event-stream/);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
test("Server-Instanz
|
|
141
|
-
// Sanity check: unter Bun wäre .server gesetzt, unter Node
|
|
142
|
-
// (wo dieser Test läuft) muss er undefined sein, sonst hätten
|
|
143
|
-
// wir eine falsche Bun-Detection.
|
|
140
|
+
test("Server-Instanz unter Bun gesetzt, unter Node undefined", async () => {
|
|
144
141
|
const h = await boot();
|
|
145
|
-
|
|
142
|
+
if (typeof Bun !== "undefined") {
|
|
143
|
+
expect(h.server).toBeDefined();
|
|
144
|
+
} else {
|
|
145
|
+
expect(h.server).toBeUndefined();
|
|
146
|
+
}
|
|
146
147
|
});
|
|
147
148
|
});
|
|
148
149
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// kein Bun → undefined, Package nicht installiert → undefined.
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import { resolveTailwindCli } from "../resolve-tailwind-cli";
|
|
6
|
+
import { canResolveTailwindStylesheet, resolveTailwindCli } from "../resolve-tailwind-cli";
|
|
7
7
|
|
|
8
8
|
describe("resolveTailwindCli", () => {
|
|
9
9
|
test("ohne Bun-Resolver → undefined (silent skip)", () => {
|
|
@@ -47,3 +47,35 @@ describe("resolveTailwindCli", () => {
|
|
|
47
47
|
expect(calls).toEqual([{ id: "@tailwindcss/cli/package.json", from: "/some/working/dir" }]);
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
describe("canResolveTailwindStylesheet", () => {
|
|
52
|
+
test("tailwindcss resolvable from entry dir → true", () => {
|
|
53
|
+
const out = canResolveTailwindStylesheet("/repo/packages/app/src/styles.css", {
|
|
54
|
+
bun: {
|
|
55
|
+
resolveSync: (id, from) => {
|
|
56
|
+
if (id === "tailwindcss" && from === "/repo/packages/app/src") {
|
|
57
|
+
return "/repo/node_modules/tailwindcss/index.css";
|
|
58
|
+
}
|
|
59
|
+
throw new Error("not found");
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
cwd: "/repo/packages/app",
|
|
63
|
+
});
|
|
64
|
+
expect(out).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("tailwindcss not resolvable from entry dir → false", () => {
|
|
68
|
+
const out = canResolveTailwindStylesheet(
|
|
69
|
+
"/cache/@cosmicdrift/kumiko-renderer-web/src/styles.css",
|
|
70
|
+
{
|
|
71
|
+
bun: {
|
|
72
|
+
resolveSync: () => {
|
|
73
|
+
throw new Error("not found");
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
cwd: "/tmp/minimal-fixture",
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
expect(out).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -491,40 +491,21 @@ describe("runProdApp", () => {
|
|
|
491
491
|
expect(res.status).toBe(200);
|
|
492
492
|
});
|
|
493
493
|
|
|
494
|
-
test("Hard Boot-Gate: pending Migration
|
|
495
|
-
//
|
|
496
|
-
// nie applied wurde. runProdApp soll mit
|
|
497
|
-
// bevor irgendetwas anderes initialisiert wird.
|
|
498
|
-
const { mkdir } = await import("node:fs/promises");
|
|
494
|
+
test("Hard Boot-Gate: pending kumiko-Migration → SchemaDriftError, kein Boot", async () => {
|
|
495
|
+
// Synthetisches kumiko/migrations-Dir mit einer checked-in Migration die
|
|
496
|
+
// nie applied wurde (kein _kumiko_migrations-Eintrag). runProdApp soll mit
|
|
497
|
+
// SchemaDriftError abbrechen bevor irgendetwas anderes initialisiert wird.
|
|
499
498
|
const driftDir = await mkdtemp(join(tmpdir(), "kumiko-drift-boot-"));
|
|
500
499
|
tempDirs.push(driftDir);
|
|
501
|
-
await mkdir(join(driftDir, "meta"), { recursive: true });
|
|
502
500
|
await writeFile(
|
|
503
|
-
join(driftDir, "
|
|
504
|
-
|
|
505
|
-
version: "7",
|
|
506
|
-
dialect: "postgresql",
|
|
507
|
-
entries: [
|
|
508
|
-
{
|
|
509
|
-
idx: 0,
|
|
510
|
-
version: "7",
|
|
511
|
-
when: 1700000000000,
|
|
512
|
-
tag: "0000_pending_migration",
|
|
513
|
-
breakpoints: true,
|
|
514
|
-
},
|
|
515
|
-
],
|
|
516
|
-
}),
|
|
501
|
+
join(driftDir, "0001_pending.sql"),
|
|
502
|
+
`CREATE TABLE "never_created_table" ("id" uuid PRIMARY KEY);`,
|
|
517
503
|
);
|
|
518
504
|
await writeFile(
|
|
519
|
-
join(driftDir, "
|
|
505
|
+
join(driftDir, ".snapshot.json"),
|
|
520
506
|
JSON.stringify({
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
schema: "",
|
|
524
|
-
name: "never_created_table",
|
|
525
|
-
columns: { id: { name: "id", type: "uuid", primaryKey: true, notNull: true } },
|
|
526
|
-
},
|
|
527
|
-
},
|
|
507
|
+
version: 1,
|
|
508
|
+
tables: [{ tableName: "never_created_table", columns: [] }],
|
|
528
509
|
}),
|
|
529
510
|
);
|
|
530
511
|
|
package/src/build-prod-bundle.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
44
44
|
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import { join, resolve } from "node:path";
|
|
47
|
+
import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
|
|
47
48
|
|
|
48
49
|
// Bun-Runtime-Check als module-level Konstante: alle Build-Schritte
|
|
49
50
|
// (Tailwind via Bun.spawn, Client-Bundle via Bun.build, Stylesheet-
|
|
@@ -138,7 +139,7 @@ export async function buildProdBundle(options: BuildProdBundleOptions = {}): Pro
|
|
|
138
139
|
// der client.tsx ein "import './foo.css'" macht — den Fall lassen
|
|
139
140
|
// wir hier raus, Tailwind ist die einzige CSS-Quelle).
|
|
140
141
|
if (stylesheet) {
|
|
141
|
-
const css = await runTailwindOnce(stylesheet);
|
|
142
|
+
const css = await runTailwindOnce(stylesheet, cwd);
|
|
142
143
|
const hash = shortHash(css);
|
|
143
144
|
const filename = `styles-${hash}.css`;
|
|
144
145
|
await writeFile(join(assetsDir, filename), css);
|
|
@@ -302,9 +303,14 @@ function resolveStylesheetEntry(
|
|
|
302
303
|
|
|
303
304
|
if (!hasBun) return undefined;
|
|
304
305
|
try {
|
|
305
|
-
|
|
306
|
+
const resolved = (
|
|
306
307
|
globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }
|
|
307
308
|
).Bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", cwd);
|
|
309
|
+
const bun = (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }).Bun;
|
|
310
|
+
if (!canResolveTailwindStylesheet(resolved, { bun, cwd })) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
return resolved;
|
|
308
314
|
} catch {
|
|
309
315
|
return undefined;
|
|
310
316
|
}
|
|
@@ -323,18 +329,30 @@ export function discoverHtmlTemplate(cwd: string): string | undefined {
|
|
|
323
329
|
// Build steps
|
|
324
330
|
// ---------------------------------------------------------------------------
|
|
325
331
|
|
|
326
|
-
async function runTailwindOnce(entry: string): Promise<string> {
|
|
332
|
+
async function runTailwindOnce(entry: string, cwd: string): Promise<string> {
|
|
327
333
|
if (!hasBun) {
|
|
328
334
|
throw new Error(
|
|
329
335
|
"[kumiko build] Tailwind one-shot requires Bun (Bun.spawn) — run via `bun run …` or `yarn kumiko build`.",
|
|
330
336
|
);
|
|
331
337
|
}
|
|
338
|
+
const bunResolver = (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } })
|
|
339
|
+
.Bun;
|
|
340
|
+
const cliPath = resolveTailwindCli({ bun: bunResolver, cwd });
|
|
341
|
+
if (cliPath === undefined) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
"[kumiko build] @tailwindcss/cli nicht auflösbar — `bun install` im App-Root ausführen.",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
if (!canResolveTailwindStylesheet(entry, { bun: bunResolver, cwd })) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`[kumiko build] tailwindcss nicht auflösbar für ${entry} — peer dependency fehlt am Stylesheet-Ort.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
332
351
|
const tmpDir = await mkdtemp(join(tmpdir(), "kumiko-build-tw-"));
|
|
333
352
|
const outPath = join(tmpDir, "styles.css");
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const proc = Bun.spawn(["bunx", "@tailwindcss/cli", "-i", entry, "-o", outPath, "--minify"], {
|
|
353
|
+
const bunPath = process.argv[0] ?? "bun";
|
|
354
|
+
const proc = Bun.spawn([bunPath, "run", cliPath, "-i", entry, "-o", outPath, "--minify"], {
|
|
355
|
+
cwd,
|
|
338
356
|
stdout: "inherit",
|
|
339
357
|
stderr: "inherit",
|
|
340
358
|
});
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
TestUsers,
|
|
31
31
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
32
32
|
import { injectSchema } from "./inject-schema";
|
|
33
|
-
import { resolveTailwindCli } from "./resolve-tailwind-cli";
|
|
33
|
+
import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
|
|
34
34
|
import { buildBunServeOptions } from "./run-prod-app";
|
|
35
35
|
import { tryHonoFirst } from "./try-hono-first";
|
|
36
36
|
|
|
@@ -362,7 +362,13 @@ export function classifyChange(filename: string): "restart" | "hot-reload" | "ig
|
|
|
362
362
|
if (!filename.endsWith(".ts") && !filename.endsWith(".tsx")) return "ignore";
|
|
363
363
|
if (filename.includes("__tests__")) return "ignore";
|
|
364
364
|
if (filename.endsWith(".test.ts") || filename.endsWith(".test.tsx")) return "ignore";
|
|
365
|
-
if (
|
|
365
|
+
if (
|
|
366
|
+
filename.endsWith(".integration.ts") ||
|
|
367
|
+
filename.endsWith(".integration.test.ts") ||
|
|
368
|
+
filename.endsWith(".e2e.ts")
|
|
369
|
+
) {
|
|
370
|
+
return "ignore";
|
|
371
|
+
}
|
|
366
372
|
// Plattformpfad-agnostisch: prüfen auf POSIX und Windows-Trenner.
|
|
367
373
|
// Wir matchen sowohl `<sep><dir><sep>` als auch trailing-`<sep><dir>`
|
|
368
374
|
// (für Watcher-Filenames die als relativer Pfad ankommen).
|
|
@@ -465,9 +471,14 @@ export function resolveStylesheet(options: CreateKumikoServerOptions): string |
|
|
|
465
471
|
return undefined;
|
|
466
472
|
}
|
|
467
473
|
try {
|
|
468
|
-
|
|
474
|
+
const resolved = (
|
|
469
475
|
globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }
|
|
470
476
|
).Bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", process.cwd());
|
|
477
|
+
const bun = (globalThis as { Bun: { resolveSync: (id: string, from: string) => string } }).Bun;
|
|
478
|
+
if (!canResolveTailwindStylesheet(resolved, { bun, cwd: process.cwd() })) {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
return resolved;
|
|
471
482
|
} catch (err) {
|
|
472
483
|
logError(
|
|
473
484
|
"[kumiko-server] couldn't auto-resolve @cosmicdrift/kumiko-renderer-web/styles.css — " +
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// testbar (Bun-Branch + Resolve-Fail-Branch) ohne den Server zu
|
|
8
8
|
// booten.
|
|
9
9
|
|
|
10
|
-
import { resolve } from "node:path";
|
|
10
|
+
import { dirname, resolve } from "node:path";
|
|
11
11
|
|
|
12
12
|
type BunResolver = { resolveSync: (id: string, from: string) => string };
|
|
13
13
|
|
|
@@ -26,3 +26,20 @@ export function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undef
|
|
|
26
26
|
return undefined;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/** Tailwind v4 resolves `@import "tailwindcss"` relative to the entry
|
|
31
|
+
* CSS file. Skip renderer-web fallback (or fail build) when peer deps
|
|
32
|
+
* aren't resolvable from that directory — e.g. Bun cache copies of
|
|
33
|
+
* `@cosmicdrift/kumiko-renderer-web` outside a hoisted node_modules tree. */
|
|
34
|
+
export function canResolveTailwindStylesheet(
|
|
35
|
+
entryCss: string,
|
|
36
|
+
deps: ResolveTailwindCliDeps,
|
|
37
|
+
): boolean {
|
|
38
|
+
if (deps.bun === undefined) return false;
|
|
39
|
+
try {
|
|
40
|
+
deps.bun.resolveSync("tailwindcss", dirname(entryCss));
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/run-prod-app.ts
CHANGED
|
@@ -69,7 +69,10 @@ import {
|
|
|
69
69
|
createSeedMigrationContext,
|
|
70
70
|
runPendingSeedMigrations,
|
|
71
71
|
} from "@cosmicdrift/kumiko-framework/es-ops";
|
|
72
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
assertKumikoSchemaCurrent,
|
|
74
|
+
SchemaDriftError,
|
|
75
|
+
} from "@cosmicdrift/kumiko-framework/migrations";
|
|
73
76
|
import {
|
|
74
77
|
createDispatcher,
|
|
75
78
|
createEntityCache,
|
|
@@ -369,7 +372,7 @@ export type RunProdAppOptions = {
|
|
|
369
372
|
* werden. CSP-Header pro Host können zusätzlich Asset-Pfade
|
|
370
373
|
* einschränken. */
|
|
371
374
|
readonly hostDispatch?: HostDispatchFn;
|
|
372
|
-
/** Pfad zu
|
|
375
|
+
/** Pfad zu kumiko/migrations für den Boot-Gate. Default "./kumiko/
|
|
373
376
|
* migrations" relativ zum process-cwd (wo die App gestartet wird —
|
|
374
377
|
* bei Container-Deploys typischerweise der App-Workspace-Root, weil
|
|
375
378
|
* WORKDIR im Dockerfile dorthin zeigt). Boot wirft SchemaDriftError
|
|
@@ -592,20 +595,20 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
592
595
|
}
|
|
593
596
|
}
|
|
594
597
|
|
|
595
|
-
// 5. Schema-Drift-Gate
|
|
596
|
-
// läuft als
|
|
597
|
-
// (a) Alle Migrations aus
|
|
598
|
-
//
|
|
599
|
-
// (b) Alle
|
|
598
|
+
// 5. Schema-Drift-Gate (drizzle-frei, kumiko/migrations). `kumiko schema
|
|
599
|
+
// apply` läuft als Deploy-Step VOR dem Container-Rollout. Boot prüft nur:
|
|
600
|
+
// (a) Alle Migrations aus kumiko/migrations/*.sql sind in
|
|
601
|
+
// _kumiko_migrations applied (+ checksum unverändert)
|
|
602
|
+
// (b) Alle Tabellen aus kumiko/migrations/.snapshot.json existieren
|
|
600
603
|
// Drift = Boot-Error mit klarer Meldung (kein Auto-Heal — mehrere
|
|
601
|
-
// Container-Replicas würden sonst race-conditionen
|
|
602
|
-
//
|
|
604
|
+
// Container-Replicas würden sonst race-conditionen fahren). Opt-out via
|
|
605
|
+
// `migrations: false` für custom Schema-Setups.
|
|
603
606
|
if (options.migrations !== false) {
|
|
604
|
-
const migrationsDir = options.migrations?.dir ?? "./
|
|
607
|
+
const migrationsDir = options.migrations?.dir ?? "./kumiko/migrations";
|
|
605
608
|
// biome-ignore lint/suspicious/noConsole: boot-time progress hint
|
|
606
609
|
console.log(`[runProdApp] checking schema drift (${migrationsDir})…`);
|
|
607
610
|
try {
|
|
608
|
-
await
|
|
611
|
+
await assertKumikoSchemaCurrent(db, migrationsDir);
|
|
609
612
|
} catch (err) {
|
|
610
613
|
if (err instanceof SchemaDriftError) {
|
|
611
614
|
// biome-ignore lint/suspicious/noConsole: terminal error message
|
/package/src/__tests__/{build-prod-bundle.integration.ts → build-prod-bundle.integration.test.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/src/__tests__/{config-seed-boot.integration.ts → config-seed-boot.integration.test.ts}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|