@cosmicdrift/kumiko-dev-server 0.48.1 → 0.50.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 +1 -1
- package/src/__tests__/config-resolver-default.integration.test.ts +97 -0
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +82 -0
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +35 -0
- package/src/__tests__/run-dev-app-boot-validation.test.ts +33 -0
- package/src/boot/apply-boot-seeds.ts +1 -0
- package/src/build-prod-bundle.ts +41 -6
- package/src/build-server-bundle.ts +1 -0
- package/src/create-kumiko-server.ts +2 -0
- package/src/kebab.ts +1 -0
- package/src/run-dev-app.ts +51 -15
- package/src/run-prod-app.ts +11 -2
- package/src/scaffold-app.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.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,97 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
configValuesTable,
|
|
4
|
+
createConfigAccessorFactory,
|
|
5
|
+
createConfigFeature,
|
|
6
|
+
createConfigResolver,
|
|
7
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
8
|
+
import { access, createTenantConfig, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
9
|
+
import {
|
|
10
|
+
setupTestStack,
|
|
11
|
+
type TestStack,
|
|
12
|
+
TestUsers,
|
|
13
|
+
unsafePushTables,
|
|
14
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
15
|
+
import { mergeConfigResolverDefault } from "../run-dev-app";
|
|
16
|
+
|
|
17
|
+
// Pins runDevApp's ENV→config-app-override wiring: mergeConfigResolverDefault
|
|
18
|
+
// builds the auth-mode configResolver-default with the ENV bridge (a key with
|
|
19
|
+
// `env:` gets its env value as the app-override default, symmetric to
|
|
20
|
+
// runProdApp), the envSource is injected (never the real process.env), and a
|
|
21
|
+
// caller-supplied configResolver still overrides the default.
|
|
22
|
+
|
|
23
|
+
const PAGE_SIZE = "devcfg:config:page-size";
|
|
24
|
+
|
|
25
|
+
const devcfgFeature = defineFeature("devcfg", (r) => {
|
|
26
|
+
r.requires("config");
|
|
27
|
+
r.config({
|
|
28
|
+
keys: {
|
|
29
|
+
pageSize: createTenantConfig("number", {
|
|
30
|
+
env: "DEVCFG_PAGE_SIZE",
|
|
31
|
+
default: 10,
|
|
32
|
+
read: access.all,
|
|
33
|
+
write: access.all,
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ctx=undefined → object form, configResolver is the only key (test boundary).
|
|
40
|
+
type ExtraObj = { configResolver: ReturnType<typeof createConfigResolver> };
|
|
41
|
+
|
|
42
|
+
let stack: TestStack;
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
stack = await setupTestStack({
|
|
46
|
+
features: [createConfigFeature(), devcfgFeature],
|
|
47
|
+
extraContext: ({ registry }) => {
|
|
48
|
+
const bootResolver = createConfigResolver();
|
|
49
|
+
return {
|
|
50
|
+
configResolver: bootResolver,
|
|
51
|
+
_configAccessorFactory: createConfigAccessorFactory(registry, bootResolver),
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
await unsafePushTables(stack.db, { configValuesTable });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await stack.cleanup();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function resolverFor(envSource: Record<string, string | undefined>) {
|
|
63
|
+
const extra = mergeConfigResolverDefault(undefined, stack.registry, envSource) as ExtraObj;
|
|
64
|
+
return extra.configResolver;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function peekPageSize(resolver: ReturnType<typeof createConfigResolver>) {
|
|
68
|
+
const keyDef = stack.registry.getConfigKey(PAGE_SIZE);
|
|
69
|
+
if (!keyDef) throw new Error("page-size key missing from registry");
|
|
70
|
+
return resolver.get(
|
|
71
|
+
PAGE_SIZE,
|
|
72
|
+
keyDef,
|
|
73
|
+
TestUsers.systemAdmin.tenantId,
|
|
74
|
+
TestUsers.systemAdmin.id,
|
|
75
|
+
stack.db,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("runDevApp configResolver-default — ENV→app-override bridge", () => {
|
|
80
|
+
test("a key with `env:` resolves to the injected env value as app-override", async () => {
|
|
81
|
+
const value = await peekPageSize(resolverFor({ DEVCFG_PAGE_SIZE: "25" }));
|
|
82
|
+
expect(value).toBe(25); // number, coerced from env — not 10 (keyDef.default)
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("no env value → falls to keyDef.default (bridge is conditional, not always-on)", async () => {
|
|
86
|
+
const value = await peekPageSize(resolverFor({}));
|
|
87
|
+
expect(value).toBe(10);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("a caller-supplied configResolver overrides the default", () => {
|
|
91
|
+
const custom = createConfigResolver();
|
|
92
|
+
const extra = mergeConfigResolverDefault({ configResolver: custom }, stack.registry, {
|
|
93
|
+
DEVCFG_PAGE_SIZE: "25",
|
|
94
|
+
}) as ExtraObj;
|
|
95
|
+
expect(extra.configResolver).toBe(custom);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Relocation-Test für den @source-Layout-Fix (#359).
|
|
2
|
+
//
|
|
3
|
+
// Der Bug: renderer-web/src/styles.css scannte seine eigenen Shell-Klassen
|
|
4
|
+
// über einen MONOREPO-relativen @source (`../../renderer-web/src`). Im
|
|
5
|
+
// Workspace löst der via Symlink auf (grün), in einem Standalone-Consumer
|
|
6
|
+
// (echtes node_modules ohne Monorepo-Geschwister) findet er nichts → unstyled
|
|
7
|
+
// prod (15KB statt 48KB). Der Fix macht die Zeile self-relativ (`./`), weil
|
|
8
|
+
// das Paket `src` shippt.
|
|
9
|
+
//
|
|
10
|
+
// Am REALEN Ort der styles.css sind `./` und `../../renderer-web/src`
|
|
11
|
+
// identisch — ein Test dort wäre grün mit Bug UND Fix. Der Diskriminator ist
|
|
12
|
+
// RELOCATION: wir kopieren die echte styles.css an einen Ort, an dem der alte
|
|
13
|
+
// Pfad tot ist und nur `./` greift. Der Ort liegt unter dem Repo-Root, damit
|
|
14
|
+
// `@import "tailwindcss"` / `react-day-picker` weiter über node_modules
|
|
15
|
+
// auflösen. Braucht Bun (Bun.spawn im Tailwind-One-Shot) — sonst silent skip.
|
|
16
|
+
|
|
17
|
+
import { describe, expect, test } from "bun:test";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
|
+
import { dirname, join, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { runTailwindOnce } from "../build-prod-bundle";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const REPO_ROOT = resolve(__dirname, "../../../..");
|
|
26
|
+
const RENDERER_WEB_STYLES = resolve(__dirname, "../../../renderer-web/src/styles.css");
|
|
27
|
+
const SELF_SOURCE = '@source "./**/*.{ts,tsx}";';
|
|
28
|
+
const MONOREPO_SOURCE = '@source "../../renderer-web/src/**/*.{ts,tsx}";';
|
|
29
|
+
|
|
30
|
+
function bunAvailable(): boolean {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync("bun", ["--version"], { stdio: "ignore" });
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe("renderer-web styles.css @source relocation (#359)", () => {
|
|
40
|
+
test("self-relative @source scans the shell standalone; monorepo path would not", async () => {
|
|
41
|
+
if (!bunAvailable()) return;
|
|
42
|
+
|
|
43
|
+
// Tailwind v4 auto-scannt nur das cwd-Verzeichnis (empirisch bestätigt:
|
|
44
|
+
// das Verzeichnis der Input-CSS wird NICHT automatisch gescannt). Im echten
|
|
45
|
+
// Standalone-Consumer ist renderer-web zudem in node_modules (gitignored) →
|
|
46
|
+
// ebenfalls nicht auto-gescannt. Die Shell-Klassen erreicht also NUR der
|
|
47
|
+
// explizite @source der styles.css. Hier mirrorn wir das robust, ohne
|
|
48
|
+
// gitignore-/node_modules-Semantik: das Paket-`src` (mit der Shell-Probe)
|
|
49
|
+
// liegt AUSSERHALB des Build-cwd — nur der self-relative `@source "./**"`
|
|
50
|
+
// der relozierten styles.css erreicht es, der monorepo-Pfad ist tot.
|
|
51
|
+
// Temp unter REPO_ROOT, damit @import "tailwindcss"/react-day-picker via
|
|
52
|
+
// node_modules auflösen.
|
|
53
|
+
const dir = await mkdtemp(join(REPO_ROOT, ".reloc-rendererweb-"));
|
|
54
|
+
const buildCwd = join(dir, "app");
|
|
55
|
+
const pkgSrc = join(dir, "pkg");
|
|
56
|
+
try {
|
|
57
|
+
const realCss = await readFile(RENDERER_WEB_STYLES, "utf8");
|
|
58
|
+
// Guard: der Fix MUSS in der Quelle stehen, sonst testet die Relocation nichts.
|
|
59
|
+
expect(realCss).toContain(SELF_SOURCE);
|
|
60
|
+
|
|
61
|
+
await mkdir(buildCwd, { recursive: true });
|
|
62
|
+
await mkdir(join(pkgSrc, "layout"), { recursive: true });
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(pkgSrc, "layout/probe.tsx"),
|
|
65
|
+
`export const P = () => <div className="min-h-screen" />;\n`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await writeFile(join(pkgSrc, "styles.css"), realCss);
|
|
69
|
+
const fixed = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
|
|
70
|
+
expect(fixed).toContain("min-h-screen");
|
|
71
|
+
|
|
72
|
+
// Diskriminator: mit dem ALTEN monorepo-relativen @source ist die Probe
|
|
73
|
+
// an diesem relozierten Ort unerreichbar → Sentinel fehlt. Beweist, dass
|
|
74
|
+
// der Fix kein No-op ist (der Test würde bei einem Revert rot).
|
|
75
|
+
await writeFile(join(pkgSrc, "styles.css"), realCss.replace(SELF_SOURCE, MONOREPO_SOURCE));
|
|
76
|
+
const buggy = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
|
|
77
|
+
expect(buggy).not.toContain("min-h-screen");
|
|
78
|
+
} finally {
|
|
79
|
+
await rm(dir, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Unit-Test für den CSS-Completeness-Guard (#359): nach dem Tailwind-One-Shot
|
|
2
|
+
// prüft der Build, ob das Output-CSS die renderer-web-Shell-Sentinel-Klasse
|
|
3
|
+
// enthält — aber NUR wenn auf das gepackte renderer-web-styles.css
|
|
4
|
+
// zurückgefallen wurde. Fehlt sie dort, würde prod unstyled rendern → laut
|
|
5
|
+
// failen mit Hinweis auf src/styles.css.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { assertRendererWebShellPresent } from "../build-prod-bundle";
|
|
9
|
+
|
|
10
|
+
const fallback = {
|
|
11
|
+
path: "/app/node_modules/renderer-web/src/styles.css",
|
|
12
|
+
isRendererWebFallback: true,
|
|
13
|
+
};
|
|
14
|
+
const appOwned = { path: "/app/src/styles.css", isRendererWebFallback: false };
|
|
15
|
+
|
|
16
|
+
describe("assertRendererWebShellPresent (#359 CSS-completeness guard)", () => {
|
|
17
|
+
test("fallback + fehlende Shell-Sentinel-Klasse → wirft mit src/styles.css-Hinweis", () => {
|
|
18
|
+
expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
|
|
19
|
+
/min-h-screen/,
|
|
20
|
+
);
|
|
21
|
+
expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
|
|
22
|
+
/src\/styles\.css/,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("fallback + Shell-Sentinel vorhanden → kein Throw", () => {
|
|
27
|
+
expect(() =>
|
|
28
|
+
assertRendererWebShellPresent(".min-h-screen{min-height:100vh}", fallback),
|
|
29
|
+
).not.toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("app-eigenes styles.css (kein Fallback) → nie asserten, auch ohne Sentinel", () => {
|
|
33
|
+
expect(() => assertRendererWebShellPresent("", appOwned)).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// runDevApp muss denselben Boot-Validator wie runProdApp ausführen (#359):
|
|
2
|
+
// eine ganze Fehlerklasse (unqualifizierte nav-/handler-QNs, unauflösbare
|
|
3
|
+
// navigate-Targets, screen-access) passierte früher den Dev-Server still und
|
|
4
|
+
// crashte erst den Prod-Pod im CrashLoopBackOff. Hier: ein Feature mit einem
|
|
5
|
+
// rowAction-navigate auf einen nie registrierten Screen — runDevApp muss
|
|
6
|
+
// SYNCHRON beim Boot werfen, bevor ein Port gebunden oder der codegen-Watcher
|
|
7
|
+
// gestartet wird (validateBoot läuft vor watchAndRegenerate).
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
11
|
+
import { runDevApp } from "../run-dev-app";
|
|
12
|
+
|
|
13
|
+
function unresolvableNavFeature() {
|
|
14
|
+
return defineFeature("shop", (r) => {
|
|
15
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
16
|
+
r.screen({
|
|
17
|
+
id: "product-list",
|
|
18
|
+
type: "entityList",
|
|
19
|
+
entity: "product",
|
|
20
|
+
columns: ["name"],
|
|
21
|
+
// "ghost-screen" wird nie via r.screen registriert → validateBoot wirft.
|
|
22
|
+
rowActions: [{ kind: "navigate", id: "edit", label: "actions.edit", screen: "ghost-screen" }],
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("runDevApp boot-validation (#359)", () => {
|
|
28
|
+
test("unresolvable navigate-target throws at boot — dev/prod parity, no port bound", async () => {
|
|
29
|
+
await expect(runDevApp({ features: [unresolvableNavFeature()] })).rejects.toThrow(
|
|
30
|
+
/navigate-target "ghost-screen" does not resolve/,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -8,6 +8,7 @@ import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
8
8
|
// missing call site (e.g. someone deletes the line from runDevApp)
|
|
9
9
|
// surfaces as a missing-helper-use in code review rather than silently
|
|
10
10
|
// shipping a server that never seeds.
|
|
11
|
+
// @wrapper-known semantic-alias
|
|
11
12
|
export async function applyBootSeeds(deps: {
|
|
12
13
|
registry: Registry;
|
|
13
14
|
db: DbConnection;
|
package/src/build-prod-bundle.ts
CHANGED
|
@@ -139,7 +139,8 @@ export async function buildProdBundle(options: BuildProdBundleOptions = {}): Pro
|
|
|
139
139
|
// der client.tsx ein "import './foo.css'" macht — den Fall lassen
|
|
140
140
|
// wir hier raus, Tailwind ist die einzige CSS-Quelle).
|
|
141
141
|
if (stylesheet) {
|
|
142
|
-
const css = await runTailwindOnce(stylesheet, cwd);
|
|
142
|
+
const css = await runTailwindOnce(stylesheet.path, cwd);
|
|
143
|
+
assertRendererWebShellPresent(css, stylesheet);
|
|
143
144
|
const hash = shortHash(css);
|
|
144
145
|
const filename = `styles-${hash}.css`;
|
|
145
146
|
await writeFile(join(assetsDir, filename), css);
|
|
@@ -284,17 +285,32 @@ export function discoverClientEntry(cwd: string): string | undefined {
|
|
|
284
285
|
return only?.name === "client" ? only.sourceFile : undefined;
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
type ResolvedStylesheet = {
|
|
289
|
+
readonly path: string;
|
|
290
|
+
/** True nur wenn wir auf das gepackte renderer-web-styles.css zurückfallen
|
|
291
|
+
* (App hat clientEntry + renderer-web-Dep, aber kein eigenes src/styles.css).
|
|
292
|
+
* Steuert den Shell-Klassen-Sentinel im Build. */
|
|
293
|
+
readonly isRendererWebFallback: boolean;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Äußerster Wrapper von DefaultAppShell/WorkspaceShell trägt diese Utility
|
|
297
|
+
// (app-layout.tsx: "flex min-h-screen …"). Echtes Tailwind-Utility (landet
|
|
298
|
+
// also im kompilierten Output) ohne CSS-Escaping → stabiler Substring-Sentinel.
|
|
299
|
+
const RENDERER_WEB_SHELL_SENTINEL = "min-h-screen";
|
|
300
|
+
|
|
287
301
|
function resolveStylesheetEntry(
|
|
288
302
|
cwd: string,
|
|
289
303
|
clientEntry: string | undefined,
|
|
290
304
|
override: BuildProdBundleOptions["stylesheet"],
|
|
291
|
-
):
|
|
305
|
+
): ResolvedStylesheet | undefined {
|
|
292
306
|
if (override === false) return undefined;
|
|
293
|
-
if (typeof override === "string")
|
|
307
|
+
if (typeof override === "string") {
|
|
308
|
+
return { path: resolve(cwd, override), isRendererWebFallback: false };
|
|
309
|
+
}
|
|
294
310
|
|
|
295
311
|
// App-eigenes styles.css schlägt den Default.
|
|
296
312
|
const local = resolve(cwd, "src/styles.css");
|
|
297
|
-
if (existsSync(local)) return local;
|
|
313
|
+
if (existsSync(local)) return { path: local, isRendererWebFallback: false };
|
|
298
314
|
|
|
299
315
|
// Sonst: nur wenn ein client da ist, fallback auf renderer-web/styles.css.
|
|
300
316
|
// Sample-Apps und Showcases nutzen das alle — gleiche Logik wie der dev-
|
|
@@ -310,12 +326,30 @@ function resolveStylesheetEntry(
|
|
|
310
326
|
if (!canResolveTailwindStylesheet(resolved, { bun, cwd })) {
|
|
311
327
|
return undefined;
|
|
312
328
|
}
|
|
313
|
-
return resolved;
|
|
329
|
+
return { path: resolved, isRendererWebFallback: true };
|
|
314
330
|
} catch {
|
|
315
331
|
return undefined;
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
334
|
|
|
335
|
+
// Build-Zeit-Guard gegen unstyled prod (#359): fällt der Build auf renderer-
|
|
336
|
+
// web/styles.css zurück und fehlt im kompilierten CSS die Shell-Sentinel-
|
|
337
|
+
// Klasse, wurden die DefaultAppShell-Styles nicht gescannt (renderer-web
|
|
338
|
+
// nicht dort installiert wo Tailwind scannt, oder ein @source-Regress).
|
|
339
|
+
// Laut failen statt ein 15KB-Image zu liefern, das in prod nackt rendert.
|
|
340
|
+
// @internal — exportiert nur für Unit-Tests.
|
|
341
|
+
export function assertRendererWebShellPresent(css: string, stylesheet: ResolvedStylesheet): void {
|
|
342
|
+
if (!stylesheet.isRendererWebFallback) return;
|
|
343
|
+
if (css.includes(RENDERER_WEB_SHELL_SENTINEL)) return;
|
|
344
|
+
throw new Error(
|
|
345
|
+
`[kumiko build] renderer-web-Fallback-CSS ohne Shell-Klasse "${RENDERER_WEB_SHELL_SENTINEL}" — ` +
|
|
346
|
+
"die DefaultAppShell/WorkspaceShell-Styles fehlen, prod würde unstyled rendern. Meist ist " +
|
|
347
|
+
"@cosmicdrift/kumiko-renderer-web nicht dort installiert wo Tailwind seine Source scannt. " +
|
|
348
|
+
'Fix: src/styles.css anlegen mit `@import "@cosmicdrift/kumiko-renderer-web/styles.css";` ' +
|
|
349
|
+
'(+ `@source "./**/*.{ts,tsx}";` um auch eigene Komponenten zu scannen).',
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
319
353
|
// @internal — exported nur für Unit-Tests.
|
|
320
354
|
export function discoverHtmlTemplate(cwd: string): string | undefined {
|
|
321
355
|
for (const candidate of ["index.html", "public/index.html"]) {
|
|
@@ -329,7 +363,8 @@ export function discoverHtmlTemplate(cwd: string): string | undefined {
|
|
|
329
363
|
// Build steps
|
|
330
364
|
// ---------------------------------------------------------------------------
|
|
331
365
|
|
|
332
|
-
|
|
366
|
+
// @internal — exportiert nur für Unit-Tests (Relocation-Test des @source-Layouts).
|
|
367
|
+
export async function runTailwindOnce(entry: string, cwd: string): Promise<string> {
|
|
333
368
|
if (!hasBun) {
|
|
334
369
|
throw new Error(
|
|
335
370
|
"[kumiko build] Tailwind one-shot requires Bun (Bun.spawn) — run via `bun run …` or `bun kumiko build`.",
|
|
@@ -267,6 +267,7 @@ export function formatServerBuildResult(
|
|
|
267
267
|
const dim = "\x1b[2m";
|
|
268
268
|
const green = "\x1b[32m";
|
|
269
269
|
const reset = "\x1b[0m";
|
|
270
|
+
// @wrapper-known semantic-alias
|
|
270
271
|
const mb = (bytes: number): string => (bytes / 1024 / 1024).toFixed(2);
|
|
271
272
|
const lines: string[] = [];
|
|
272
273
|
lines.push("");
|
|
@@ -49,8 +49,10 @@ const hasBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
|
|
|
49
49
|
type BunServer = typeof Bun extends undefined ? unknown : ReturnType<typeof Bun.serve>;
|
|
50
50
|
|
|
51
51
|
// biome-ignore lint/suspicious/noConsole: dev-server status logging
|
|
52
|
+
// @wrapper-known semantic-alias
|
|
52
53
|
const logInfo = (msg: string): void => console.log(msg);
|
|
53
54
|
// biome-ignore lint/suspicious/noConsole: dev-server error logging
|
|
55
|
+
// @wrapper-known semantic-alias
|
|
54
56
|
const logError = (...args: unknown[]): void => console.error(...args);
|
|
55
57
|
|
|
56
58
|
/** Multi-Entry-Mode für Apps die mehrere getrennte Bundles ausliefern
|
package/src/kebab.ts
CHANGED
package/src/run-dev-app.ts
CHANGED
|
@@ -18,7 +18,10 @@ import {
|
|
|
18
18
|
type SeedAdminOptions,
|
|
19
19
|
seedAdmin,
|
|
20
20
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
buildEnvConfigOverrides,
|
|
23
|
+
createConfigResolver,
|
|
24
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
22
25
|
import {
|
|
23
26
|
createSessionCallbacks,
|
|
24
27
|
type SessionCallbacks,
|
|
@@ -26,12 +29,15 @@ import {
|
|
|
26
29
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
27
30
|
import type { SessionMetadata } from "@cosmicdrift/kumiko-framework/api";
|
|
28
31
|
import {
|
|
32
|
+
createRegistry,
|
|
29
33
|
type EffectiveFeaturesResolver,
|
|
30
34
|
type FeatureDefinition,
|
|
31
35
|
findTierResolverUsage,
|
|
36
|
+
type Registry,
|
|
32
37
|
type SessionUser,
|
|
33
38
|
type TenantId,
|
|
34
39
|
type TierResolverPlugin,
|
|
40
|
+
validateBoot,
|
|
35
41
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
36
42
|
import type { TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
37
43
|
import { applyBootSeeds } from "./boot/apply-boot-seeds";
|
|
@@ -140,6 +146,11 @@ export type RunDevAppOptions = {
|
|
|
140
146
|
/** Extra-AppContext-Keys. Im auth-mode wird `configResolver` automatisch
|
|
141
147
|
* hinzugefügt — kein Override durch den Caller nötig. */
|
|
142
148
|
readonly extraContext?: CreateKumikoServerOptions["extraContext"];
|
|
149
|
+
/** Env-Quelle für die ENV→config-app-override-Brücke (Keys mit `env:`
|
|
150
|
+
* bekommen ihren env-Wert als app-override-Default — symmetrisch zu
|
|
151
|
+
* runProdApp). Default `process.env`. Injizierbar als Test-Seam, damit
|
|
152
|
+
* Tests eigene env-Werte reichen statt `process.env` global zu mutieren. */
|
|
153
|
+
readonly envSource?: Record<string, string | undefined>;
|
|
143
154
|
/** Anonymous-Access für Public-Endpoints — Requests ohne JWT laufen
|
|
144
155
|
* als Pseudo-User mit Rolle `anonymous` durch, wenn der Handler die
|
|
145
156
|
* Rolle in `access.roles` führt. */
|
|
@@ -158,6 +169,23 @@ export type RunDevAppOptions = {
|
|
|
158
169
|
};
|
|
159
170
|
|
|
160
171
|
export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServerHandle> {
|
|
172
|
+
// Auto-mix Standard-Features im auth-mode via composeFeatures (single
|
|
173
|
+
// source of truth — auch runProdApp und der per-app drizzle-Schema-
|
|
174
|
+
// Generator nutzen denselben Helper, damit Migration und Runtime nie
|
|
175
|
+
// auseinanderdriften können).
|
|
176
|
+
const composeAuthOptions = buildComposeAuthOptions(options.auth);
|
|
177
|
+
const features = composeFeatures(options.features, {
|
|
178
|
+
includeBundled: !!options.auth,
|
|
179
|
+
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Boot-Validation als allererstes — vor fs-Watcher und Server. Dieselbe
|
|
183
|
+
// Fehlerklasse (unqualifizierte nav-/handler-QNs, screen-access etc.),
|
|
184
|
+
// die früher nur runProdApp fing und sonst erst den Prod-Pod im
|
|
185
|
+
// CrashLoopBackOff sterben ließ (#359). Wirft synchron, bevor ein
|
|
186
|
+
// Socket oder Watcher (codegen-Write) aufgeht.
|
|
187
|
+
validateBoot(features);
|
|
188
|
+
|
|
161
189
|
// Codegen + File-Watcher — schreibt `<appRoot>/.kumiko/types.generated.d.ts`
|
|
162
190
|
// + `define.ts` aus den r.defineEvent-Aufrufen der App, einmal beim
|
|
163
191
|
// Boot UND danach bei jeder relevanten Änderung unter `<appRoot>/src/`.
|
|
@@ -170,16 +198,6 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
170
198
|
// process-exit räumt fs.watch-handles auf).
|
|
171
199
|
watchAndRegenerate({ appRoot: process.cwd() });
|
|
172
200
|
|
|
173
|
-
// Auto-mix Standard-Features im auth-mode via composeFeatures (single
|
|
174
|
-
// source of truth — auch runProdApp und der per-app drizzle-Schema-
|
|
175
|
-
// Generator nutzen denselben Helper, damit Migration und Runtime nie
|
|
176
|
-
// auseinanderdriften können).
|
|
177
|
-
const composeAuthOptions = buildComposeAuthOptions(options.auth);
|
|
178
|
-
const features = composeFeatures(options.features, {
|
|
179
|
-
includeBundled: !!options.auth,
|
|
180
|
-
...(composeAuthOptions && { authOptions: composeAuthOptions }),
|
|
181
|
-
});
|
|
182
|
-
|
|
183
201
|
// Sprint-8a Tier-Composition auto-wire: scan features for a
|
|
184
202
|
// tenantTierResolver-extension. If found AND user didn't supply own
|
|
185
203
|
// effectiveFeatures, we wire a late-bound wrapper here and fill it
|
|
@@ -210,9 +228,17 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
210
228
|
// configResolver-default fürs config-feature — im auth-mode immer
|
|
211
229
|
// hinzufügen, im no-auth-mode dem Caller überlassen. Factory-form
|
|
212
230
|
// wird gewrap't damit der spread auf das aufgerufene Result greift,
|
|
213
|
-
// nicht auf die function selbst (no-op).
|
|
231
|
+
// nicht auf die function selbst (no-op). Die ENV→config-Brücke (Keys mit
|
|
232
|
+
// `env:` → app-override-Default) läuft symmetrisch zu runProdApp; die
|
|
233
|
+
// throwaway-Registry hier extrahiert nur die config-Keys, weil der
|
|
234
|
+
// configResolver vor dem Server-Boot konstruiert wird (stack.registry
|
|
235
|
+
// gibt's erst onAfterSetup) — createKumikoServer baut intern seine eigene.
|
|
214
236
|
const extraContext = options.auth
|
|
215
|
-
? mergeConfigResolverDefault(
|
|
237
|
+
? mergeConfigResolverDefault(
|
|
238
|
+
options.extraContext,
|
|
239
|
+
createRegistry(features),
|
|
240
|
+
options.envSource ?? process.env,
|
|
241
|
+
)
|
|
216
242
|
: options.extraContext;
|
|
217
243
|
|
|
218
244
|
// Sessions opt-in: Holder lebt im closure, `createSessionCallbacks`
|
|
@@ -343,10 +369,20 @@ export async function runDevApp(options: RunDevAppOptions): Promise<KumikoServer
|
|
|
343
369
|
});
|
|
344
370
|
}
|
|
345
371
|
|
|
346
|
-
|
|
372
|
+
// Exported for the wiring-contract test (config-resolver-default.integration):
|
|
373
|
+
// pins that the dev configResolver-default carries the ENV→app-override bridge
|
|
374
|
+
// and that a caller-supplied configResolver still overrides it. Not re-exported
|
|
375
|
+
// from the package barrel (index.ts lists only runDevApp).
|
|
376
|
+
export function mergeConfigResolverDefault(
|
|
347
377
|
ctx: CreateKumikoServerOptions["extraContext"],
|
|
378
|
+
registry: Registry,
|
|
379
|
+
envSource: Record<string, string | undefined>,
|
|
348
380
|
): CreateKumikoServerOptions["extraContext"] {
|
|
349
|
-
const defaults = {
|
|
381
|
+
const defaults = {
|
|
382
|
+
configResolver: createConfigResolver({
|
|
383
|
+
appOverrides: buildEnvConfigOverrides(registry, envSource),
|
|
384
|
+
}),
|
|
385
|
+
};
|
|
350
386
|
if (ctx === undefined) return defaults;
|
|
351
387
|
if (typeof ctx === "function") {
|
|
352
388
|
return (deps) => ({ ...defaults, ...ctx(deps) });
|
package/src/run-prod-app.ts
CHANGED
|
@@ -36,7 +36,10 @@ import {
|
|
|
36
36
|
type SeedAdminOptions,
|
|
37
37
|
seedAdmin,
|
|
38
38
|
} from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
buildEnvConfigOverrides,
|
|
41
|
+
createConfigResolver,
|
|
42
|
+
} from "@cosmicdrift/kumiko-bundled-features/config";
|
|
40
43
|
import { createSessionCallbacks } from "@cosmicdrift/kumiko-bundled-features/sessions";
|
|
41
44
|
import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
42
45
|
import { UserQueries } from "@cosmicdrift/kumiko-bundled-features/user";
|
|
@@ -668,7 +671,12 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
668
671
|
? options.extraContext(deps)
|
|
669
672
|
: (options.extraContext ?? {});
|
|
670
673
|
const extraContext = options.auth
|
|
671
|
-
? {
|
|
674
|
+
? {
|
|
675
|
+
configResolver: createConfigResolver({
|
|
676
|
+
appOverrides: buildEnvConfigOverrides(registry, envSource),
|
|
677
|
+
}),
|
|
678
|
+
...resolvedExtraContext,
|
|
679
|
+
}
|
|
672
680
|
: resolvedExtraContext;
|
|
673
681
|
const resolvedAnonymousAccess =
|
|
674
682
|
typeof options.anonymousAccess === "function"
|
|
@@ -825,6 +833,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
825
833
|
seedsDir: options.seedsDir,
|
|
826
834
|
appliedBy: "boot",
|
|
827
835
|
registry, // → dry-run-validator catched handler-QN-typos vor dem write
|
|
836
|
+
// @wrapper-known semantic-alias
|
|
828
837
|
createContext: (dbRunner: DbRunner) =>
|
|
829
838
|
createSeedMigrationContext({ dispatcher: seedDispatcher, dbRunner }),
|
|
830
839
|
});
|
package/src/scaffold-app.ts
CHANGED
|
@@ -73,6 +73,7 @@ export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
|
|
|
73
73
|
return { destination, files, appName: options.name };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// @wrapper-known semantic-alias
|
|
76
77
|
function write(path: string, content: string): void {
|
|
77
78
|
writeFileSync(path, content);
|
|
78
79
|
}
|
|
@@ -334,6 +335,7 @@ function deriveTenantId(name: string): string {
|
|
|
334
335
|
state ^= ch.charCodeAt(0);
|
|
335
336
|
state = Math.imul(state, 16777619) >>> 0;
|
|
336
337
|
}
|
|
338
|
+
// @wrapper-known semantic-alias
|
|
337
339
|
const hex = (n: number, len: number): string => n.toString(16).padStart(len, "0").slice(0, len);
|
|
338
340
|
const a = hex(state, 8);
|
|
339
341
|
state ^= state << 13;
|