@cosmicdrift/kumiko-dev-server 0.24.0 → 0.24.1
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.24.
|
|
3
|
+
"version": "0.24.1",
|
|
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,104 @@
|
|
|
1
|
+
// Regression: the boot-path must read the injected `envSource`, not the real
|
|
2
|
+
// process.env. Boot-mode (KUMIKO_DRY_RUN_ENV=boot) validates wiring + builds
|
|
3
|
+
// the registry, then tears down the lazy DB/Redis clients before any socket
|
|
4
|
+
// opens — so this runs without a real Postgres/Redis (same as the CI boot
|
|
5
|
+
// smoke). Before the fix, requireEnv/readEnv read process.env directly, so the
|
|
6
|
+
// required-var test would throw "required env var DATABASE_URL is missing" and
|
|
7
|
+
// the PORT test would bind the default instead of the injected port.
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
createBooleanField,
|
|
12
|
+
createEntity,
|
|
13
|
+
createTextField,
|
|
14
|
+
defineFeature,
|
|
15
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { runProdApp } from "../run-prod-app";
|
|
18
|
+
|
|
19
|
+
const probeEntity = createEntity({
|
|
20
|
+
fields: {
|
|
21
|
+
name: createTextField({ required: true }),
|
|
22
|
+
active: createBooleanField({ default: true }),
|
|
23
|
+
},
|
|
24
|
+
table: "env_source_probe",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const probeFeature = defineFeature("env-source-probe", (r) => {
|
|
28
|
+
r.entity("widget", probeEntity);
|
|
29
|
+
r.queryHandler({
|
|
30
|
+
name: "ping",
|
|
31
|
+
schema: z.object({}),
|
|
32
|
+
access: { roles: ["anonymous"] },
|
|
33
|
+
handler: async () => ({ pong: true }),
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Cleared from process.env so the test fully controls config via envSource.
|
|
38
|
+
// DATABASE_URL/REDIS_URL/JWT_SECRET are required (their read throws pre-fix);
|
|
39
|
+
// PORT is non-throwing, cleared only so ambient PORT can't mask the second
|
|
40
|
+
// test's "PORT comes from envSource" assertion.
|
|
41
|
+
const CLEARED_VARS = ["DATABASE_URL", "REDIS_URL", "JWT_SECRET", "PORT"] as const;
|
|
42
|
+
|
|
43
|
+
const DUMMY_ENV = {
|
|
44
|
+
KUMIKO_DRY_RUN_ENV: "boot",
|
|
45
|
+
DATABASE_URL: "postgres://smoke:smoke@127.0.0.1:1/smoke",
|
|
46
|
+
REDIS_URL: "redis://127.0.0.1:1",
|
|
47
|
+
JWT_SECRET: "smokesmokesmokesmokesmokesmokesmokesmoke",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
describe("runProdApp boot-mode env-source", () => {
|
|
51
|
+
const saved: Record<string, string | undefined> = {};
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
for (const k of CLEARED_VARS) {
|
|
55
|
+
saved[k] = process.env[k];
|
|
56
|
+
delete process.env[k];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
for (const k of CLEARED_VARS) {
|
|
62
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
63
|
+
else process.env[k] = saved[k];
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("boots from injected envSource even when process.env lacks the required vars", async () => {
|
|
68
|
+
const handle = await runProdApp({
|
|
69
|
+
features: [probeFeature],
|
|
70
|
+
autoListen: false,
|
|
71
|
+
migrations: false,
|
|
72
|
+
envSource: { ...DUMMY_ENV },
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Boot-mode with an injected envSource returns an inert dry-run handle.
|
|
76
|
+
expect(handle).toBeDefined();
|
|
77
|
+
expect(typeof handle.stop).toBe("function");
|
|
78
|
+
await handle.stop();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("resolves PORT from envSource, not process.env", async () => {
|
|
82
|
+
const logs: string[] = [];
|
|
83
|
+
const originalLog = console.log;
|
|
84
|
+
console.log = (...args: unknown[]) => {
|
|
85
|
+
logs.push(args.map(String).join(" "));
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
const handle = await runProdApp({
|
|
89
|
+
features: [probeFeature],
|
|
90
|
+
autoListen: false,
|
|
91
|
+
migrations: false,
|
|
92
|
+
envSource: { ...DUMMY_ENV, PORT: "8123" },
|
|
93
|
+
});
|
|
94
|
+
await handle.stop();
|
|
95
|
+
} finally {
|
|
96
|
+
console.log = originalLog;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// The boot logs "booting Kumiko stack on port <port>" — pre-fix this read
|
|
100
|
+
// process.env["PORT"] (deleted here) and would log the 3000 default.
|
|
101
|
+
expect(logs.some((line) => line.includes("port 8123"))).toBe(true);
|
|
102
|
+
expect(logs.some((line) => line.includes("port 3000"))).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
2
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -205,10 +205,17 @@ describe("scaffoldDeploy", () => {
|
|
|
205
205
|
expect(df).not.toContain("ENV GITHUB_TOKEN");
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
it("malformed package.json
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
it("malformed package.json warns + defaults to no private deps (mis-detection is visible)", () => {
|
|
209
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
210
|
+
try {
|
|
211
|
+
writeFileSync(join(tmp, "package.json"), "{ this is not json");
|
|
212
|
+
const result = scaffoldDeploy({ appName: "broken", destination: tmp });
|
|
213
|
+
expect(result.detected.hasPrivateGhPackages).toBe(false);
|
|
214
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
215
|
+
expect(warn.mock.calls[0]?.[0]).toContain("is not valid JSON");
|
|
216
|
+
} finally {
|
|
217
|
+
warn.mockRestore();
|
|
218
|
+
}
|
|
212
219
|
});
|
|
213
220
|
});
|
|
214
221
|
});
|
package/src/env-schema.ts
CHANGED
|
@@ -47,7 +47,7 @@ export const frameworkCoreEnvSchema = z.object({
|
|
|
47
47
|
),
|
|
48
48
|
|
|
49
49
|
// `z.string().optional()` (not `z.literal("1")`) — the run-prod-app
|
|
50
|
-
// call-site (`
|
|
50
|
+
// call-site (`envSource["KUMIKO_SKIP_ES_OPS"] !== "1"`) ignores any
|
|
51
51
|
// value other than literal "1". A stricter schema would reject e.g.
|
|
52
52
|
// "true" / "yes" that the runtime silently ignores, surfacing
|
|
53
53
|
// boot-errors for inputs the framework doesn't actually care about.
|
package/src/run-prod-app.ts
CHANGED
|
@@ -110,8 +110,11 @@ export function buildBunServeOptions(
|
|
|
110
110
|
|
|
111
111
|
// Strict env-var read. Throws with a clear hint when missing — better
|
|
112
112
|
// than discovering a Postgres-connection-refused 30s into the boot.
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
// `src` defaults to process.env but is threaded from the caller's envSource
|
|
114
|
+
// so the boot-path reads the SAME env-quelle that was validated above —
|
|
115
|
+
// injected dummies in test-mode must not silently fall back to process.env.
|
|
116
|
+
function requireEnv(name: string, src: Record<string, string | undefined> = process.env): string {
|
|
117
|
+
const value = src[name];
|
|
115
118
|
if (value === undefined || value === "") {
|
|
116
119
|
throw new Error(
|
|
117
120
|
`runProdApp: required env var "${name}" is missing or empty. ` +
|
|
@@ -123,8 +126,11 @@ function requireEnv(name: string): string {
|
|
|
123
126
|
|
|
124
127
|
// Optional env helper — returns undefined for missing, string for set.
|
|
125
128
|
// Used for KUMIKO_INSTANCE_ID, JWT_ISSUER and other "nice to have" knobs.
|
|
126
|
-
function readEnv(
|
|
127
|
-
|
|
129
|
+
function readEnv(
|
|
130
|
+
name: string,
|
|
131
|
+
src: Record<string, string | undefined> = process.env,
|
|
132
|
+
): string | undefined {
|
|
133
|
+
const value = src[name];
|
|
128
134
|
return value === undefined || value === "" ? undefined : value;
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -535,12 +541,12 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
535
541
|
// 2. Env-vars: fail-fast. Better a 0s boot crash with a clear error
|
|
536
542
|
// than a 30s timeout chasing a Postgres connection that was never
|
|
537
543
|
// configured.
|
|
538
|
-
const databaseUrl = requireEnv("DATABASE_URL");
|
|
539
|
-
const redisUrl = requireEnv("REDIS_URL");
|
|
540
|
-
const jwtSecret = requireEnv("JWT_SECRET");
|
|
541
|
-
const jwtIssuer = readEnv("JWT_ISSUER");
|
|
542
|
-
const instanceId = readEnv("KUMIKO_INSTANCE_ID");
|
|
543
|
-
const port = options.port ?? Number.parseInt(
|
|
544
|
+
const databaseUrl = requireEnv("DATABASE_URL", envSource);
|
|
545
|
+
const redisUrl = requireEnv("REDIS_URL", envSource);
|
|
546
|
+
const jwtSecret = requireEnv("JWT_SECRET", envSource);
|
|
547
|
+
const jwtIssuer = readEnv("JWT_ISSUER", envSource);
|
|
548
|
+
const instanceId = readEnv("KUMIKO_INSTANCE_ID", envSource);
|
|
549
|
+
const port = options.port ?? Number.parseInt(envSource["PORT"] ?? "3000", 10);
|
|
544
550
|
|
|
545
551
|
// biome-ignore lint/suspicious/noConsole: boot-time progress hint, no logger configured this early
|
|
546
552
|
console.log(`[runProdApp] booting Kumiko stack on port ${port}…`);
|
|
@@ -774,7 +780,7 @@ export async function runProdApp(options: RunProdAppOptions): Promise<ProdAppHan
|
|
|
774
780
|
// if-missing"-Schicht; seed-migrations sind die "diff-and-update"-
|
|
775
781
|
// Schicht für Drift den existing Seeds nicht erfassen können (z.B.
|
|
776
782
|
// Membership-Roles-Change nach initialer Seed-Erstellung).
|
|
777
|
-
if (options.seedsDir !== undefined &&
|
|
783
|
+
if (options.seedsDir !== undefined && envSource["KUMIKO_SKIP_ES_OPS"] !== "1") {
|
|
778
784
|
await createEsOperationsTable(db);
|
|
779
785
|
const seedDispatcher = createDispatcher(registry, {
|
|
780
786
|
db,
|
package/src/scaffold-app.ts
CHANGED
|
@@ -249,7 +249,7 @@ function renderMain(appName: string): string {
|
|
|
249
249
|
"// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
|
|
250
250
|
"// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
|
|
251
251
|
"// (siehe @cosmicdrift/kumiko-dev-server runProdApp). Echter Dev-Boot",
|
|
252
|
-
"// passiert via `
|
|
252
|
+
"// passiert via `bunx kumiko dev` (in-repo dev-tool) mit Docker-stack — DX-1.0 deckt nur",
|
|
253
253
|
"// den boot-mode-Pfad ab; `kumiko dev` kommt in einer späteren DX-Phase.",
|
|
254
254
|
"",
|
|
255
255
|
"",
|
package/src/scaffold-deploy.ts
CHANGED
|
@@ -143,8 +143,14 @@ function detectOptionalSurfaces(sourceDir: string): ScaffoldDeployDetected {
|
|
|
143
143
|
hasPrivateGhPackages = Object.keys(allDeps).some((d) =>
|
|
144
144
|
d.startsWith("@cosmicdriftgamestudio/"),
|
|
145
145
|
);
|
|
146
|
-
} catch {
|
|
147
|
-
// malformed package.json — assume no private packages, app-author can
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// malformed package.json — assume no private packages, app-author can
|
|
148
|
+
// override via Dockerfile. Warn so a silent mis-detection (later YN0041
|
|
149
|
+
// on yarn install) is traceable to the scaffold step.
|
|
150
|
+
// biome-ignore lint/suspicious/noConsole: scaffold visibility for skipped private-package detection
|
|
151
|
+
console.warn(
|
|
152
|
+
`scaffoldDeploy: package.json at ${pkgJsonPath} is not valid JSON — private-GH-packages detection skipped (${err instanceof Error ? err.message : String(err)})`,
|
|
153
|
+
);
|
|
148
154
|
}
|
|
149
155
|
}
|
|
150
156
|
return { hasSeeds, hasPrivateGhPackages };
|