@cosmicdrift/kumiko-dev-server 0.322.0 → 0.323.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-build.ts +25 -19
- package/bin/kumiko-init-deploy.ts +21 -0
- package/package.json +6 -5
- package/src/__tests__/init-deploy-cli.test.ts +113 -0
- package/src/__tests__/scaffold-deploy.test.ts +219 -7
- package/src/build.ts +3 -1
- package/src/changes.json +26 -0
- package/src/create-kumiko-server.ts +12 -10
- package/src/index.ts +9 -1
- package/src/init-deploy-cli.ts +148 -0
- package/src/run-dev-app.ts +14 -10
- package/src/scaffold-app-feature.ts +6 -1
- package/src/scaffold-deploy.ts +224 -36
- package/templates/deploy/Dockerfile.template +32 -20
- package/templates/deploy/migrate-step.sh.template +19 -1
package/bin/kumiko-build.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// kumiko-build — Production-Build für Kumiko-Apps. Convention-driven:
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// package.json "kumiko.clientEntries"/"kumiko.clientEntry",
|
|
5
|
+
// or src/client.tsx | src/styles.css | public/ → Client-Bundle (dist/)
|
|
6
|
+
// bin/main.ts → Server-Bundle (dist-server/)
|
|
6
7
|
//
|
|
7
8
|
// Beide werden gebaut wenn die jeweiligen Conventions getroffen sind, sonst
|
|
8
9
|
// übersprungen. Ein Workspace mit nur bin/main.ts kriegt nur den Server-
|
|
@@ -20,11 +21,12 @@ import { join, resolve } from "node:path";
|
|
|
20
21
|
import {
|
|
21
22
|
buildProdBundle,
|
|
22
23
|
buildServerBundle,
|
|
23
|
-
discoverClientEntries,
|
|
24
24
|
discoverServerEntry,
|
|
25
25
|
formatBuildResult,
|
|
26
26
|
formatServerBuildResult,
|
|
27
|
+
readClientEntriesConfig,
|
|
27
28
|
readExtraRuntimeExternals,
|
|
29
|
+
resolveClientEntries,
|
|
28
30
|
} from "../src/build";
|
|
29
31
|
import { formatScanWarning, runCodegen } from "../src/codegen";
|
|
30
32
|
|
|
@@ -35,23 +37,27 @@ const red = "\x1b[31m";
|
|
|
35
37
|
const yellow = "\x1b[33m";
|
|
36
38
|
const reset = "\x1b[0m";
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
40
|
+
try {
|
|
41
|
+
// Read once up front — resolveClientEntries can throw (mutual exclusion,
|
|
42
|
+
// malformed declaration, unmigrated legacy client-*.tsx files) and that
|
|
43
|
+
// must surface through the same ✗/exit(1) path as every other build error.
|
|
44
|
+
const declaredClientEntries = readClientEntriesConfig(cwd);
|
|
45
|
+
const hasClient =
|
|
46
|
+
resolveClientEntries(cwd, declaredClientEntries).length > 0 ||
|
|
47
|
+
existsSync(join(cwd, "src/styles.css")) ||
|
|
48
|
+
existsSync(join(cwd, "public")) ||
|
|
49
|
+
existsSync(join(cwd, "index.html"));
|
|
50
|
+
const hasServer = discoverServerEntry(cwd) !== undefined;
|
|
44
51
|
|
|
45
|
-
if (!hasClient && !hasServer) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
52
|
+
if (!hasClient && !hasServer) {
|
|
53
|
+
// biome-ignore lint/suspicious/noConsole: CLI-Output, einziger Weg
|
|
54
|
+
console.error(
|
|
55
|
+
`\n ${yellow}!${reset} Nichts zu bauen in ${cwd}.\n` +
|
|
56
|
+
` Convention: src/client.tsx oder bin/main.ts erwartet.\n`,
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
53
60
|
|
|
54
|
-
try {
|
|
55
61
|
// Codegen-Pass vor dem Build — sicherstellt dass `.kumiko/define.ts`
|
|
56
62
|
// und `types.generated.d.ts` synchron mit den r.defineEvent-Aufrufen
|
|
57
63
|
// sind. Ohne diesen Pass würde ein veraltetes Wrapper-File ein
|
|
@@ -67,7 +73,7 @@ try {
|
|
|
67
73
|
|
|
68
74
|
if (hasClient) {
|
|
69
75
|
const t0 = performance.now();
|
|
70
|
-
const result = await buildProdBundle({ cwd });
|
|
76
|
+
const result = await buildProdBundle({ cwd, ...declaredClientEntries });
|
|
71
77
|
const ms = Math.round(performance.now() - t0);
|
|
72
78
|
// biome-ignore lint/suspicious/noConsole: CLI-Output, einziger Weg
|
|
73
79
|
console.log(formatBuildResult(result, ms));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// biome-ignore-all lint/suspicious/noConsole: CLI script, console is the feature.
|
|
3
|
+
// kumiko-init-deploy — Scaffold deploy/{Dockerfile,Dockerfile.dockerignore,
|
|
4
|
+
// migrate-step.sh} from the canonical templates shipped with this package.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// bunx kumiko-init-deploy --app <name> [--port <n>] [--github-org <org>] [--out <dir>] [--force]
|
|
8
|
+
// bunx kumiko-init-deploy --check # drift check, exit 1 if deploy/ is stale
|
|
9
|
+
|
|
10
|
+
import { runInitDeployCli } from "../src/init-deploy-cli";
|
|
11
|
+
|
|
12
|
+
// Only run when executed as a CLI, not when imported (e.g. from tests that
|
|
13
|
+
// exercise runInitDeployCli directly).
|
|
14
|
+
if (import.meta.main) {
|
|
15
|
+
const exitCode = await runInitDeployCli({
|
|
16
|
+
argv: process.argv.slice(2),
|
|
17
|
+
cwd: process.cwd(),
|
|
18
|
+
out: { log: console.log, err: console.error },
|
|
19
|
+
});
|
|
20
|
+
process.exit(exitCode);
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.323.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>",
|
|
@@ -54,15 +54,16 @@
|
|
|
54
54
|
"bin": {
|
|
55
55
|
"kumiko-build": "./bin/kumiko-build.ts",
|
|
56
56
|
"kumiko-dev": "./bin/kumiko-dev.ts",
|
|
57
|
+
"kumiko-init-deploy": "./bin/kumiko-init-deploy.ts",
|
|
57
58
|
"kumiko-schema": "./bin/kumiko-schema.ts",
|
|
58
59
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts",
|
|
59
60
|
"kumiko-upgrade": "./bin/kumiko-upgrade.ts"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
63
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
64
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
65
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
63
|
+
"@cosmicdrift/kumiko-bundled-features": "0.323.0",
|
|
64
|
+
"@cosmicdrift/kumiko-framework": "0.323.0",
|
|
65
|
+
"@cosmicdrift/kumiko-headless": "0.323.0",
|
|
66
|
+
"@cosmicdrift/kumiko-server-runtime": "0.323.0",
|
|
66
67
|
"ts-morph": "^28.0.0"
|
|
67
68
|
},
|
|
68
69
|
"publishConfig": {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runInitDeployCli } from "../init-deploy-cli";
|
|
6
|
+
|
|
7
|
+
function makeOut() {
|
|
8
|
+
const logs: string[] = [];
|
|
9
|
+
const errs: string[] = [];
|
|
10
|
+
return {
|
|
11
|
+
out: { log: (m: string) => logs.push(m), err: (m: string) => errs.push(m) },
|
|
12
|
+
logs,
|
|
13
|
+
errs,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("runInitDeployCli", () => {
|
|
18
|
+
let tmp: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmp = mkdtempSync(join(tmpdir(), "kumiko-init-deploy-cli-"));
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("scaffolds with an explicit --app and reports exit 0", async () => {
|
|
28
|
+
const { out, logs } = makeOut();
|
|
29
|
+
const code = await runInitDeployCli({ argv: ["--app", "myapp"], cwd: tmp, out });
|
|
30
|
+
expect(code).toBe(0);
|
|
31
|
+
expect(logs.some((l) => l.includes("Deploy scaffolding generated"))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("defaults --app from package.json name, stripping the scope", async () => {
|
|
35
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "@cosmicdrift/some-app" }));
|
|
36
|
+
const { out, logs } = makeOut();
|
|
37
|
+
const code = await runInitDeployCli({ argv: [], cwd: tmp, out });
|
|
38
|
+
expect(code).toBe(0);
|
|
39
|
+
expect(logs.some((l) => l.includes("some-app"))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("fails with a usage error when --app is missing and package.json has no name", async () => {
|
|
43
|
+
const { out, errs } = makeOut();
|
|
44
|
+
const code = await runInitDeployCli({ argv: [], cwd: tmp, out });
|
|
45
|
+
expect(code).toBe(1);
|
|
46
|
+
expect(errs.some((e) => e.includes("--app <name> is required"))).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("--check and --force are mutually exclusive (exit 2)", async () => {
|
|
50
|
+
const { out, errs } = makeOut();
|
|
51
|
+
const code = await runInitDeployCli({
|
|
52
|
+
argv: ["--app", "myapp", "--check", "--force"],
|
|
53
|
+
cwd: tmp,
|
|
54
|
+
out,
|
|
55
|
+
});
|
|
56
|
+
expect(code).toBe(2);
|
|
57
|
+
expect(errs.some((e) => e.includes("mutually exclusive"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("--check exits 0 when deploy/ is already in sync", async () => {
|
|
61
|
+
const first = await runInitDeployCli({
|
|
62
|
+
argv: ["--app", "myapp"],
|
|
63
|
+
cwd: tmp,
|
|
64
|
+
out: makeOut().out,
|
|
65
|
+
});
|
|
66
|
+
expect(first).toBe(0);
|
|
67
|
+
const { out, logs } = makeOut();
|
|
68
|
+
const code = await runInitDeployCli({ argv: ["--app", "myapp", "--check"], cwd: tmp, out });
|
|
69
|
+
expect(code).toBe(0);
|
|
70
|
+
expect(logs.some((l) => l.includes("in sync"))).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("--check exits 1 and lists drifted files when deploy/ is missing or stale", async () => {
|
|
74
|
+
const { out: outMissing, errs: errsMissing } = makeOut();
|
|
75
|
+
const missingCode = await runInitDeployCli({
|
|
76
|
+
argv: ["--app", "myapp", "--check"],
|
|
77
|
+
cwd: tmp,
|
|
78
|
+
out: outMissing,
|
|
79
|
+
});
|
|
80
|
+
expect(missingCode).toBe(1);
|
|
81
|
+
expect(errsMissing.some((e) => e.includes("missing"))).toBe(true);
|
|
82
|
+
|
|
83
|
+
await runInitDeployCli({ argv: ["--app", "myapp"], cwd: tmp, out: makeOut().out });
|
|
84
|
+
writeFileSync(join(tmp, "deploy", "Dockerfile"), "# stale");
|
|
85
|
+
const { out: outStale, errs: errsStale } = makeOut();
|
|
86
|
+
const staleCode = await runInitDeployCli({
|
|
87
|
+
argv: ["--app", "myapp", "--check"],
|
|
88
|
+
cwd: tmp,
|
|
89
|
+
out: outStale,
|
|
90
|
+
});
|
|
91
|
+
expect(staleCode).toBe(1);
|
|
92
|
+
expect(errsStale.some((e) => e.includes("differs"))).toBe(true);
|
|
93
|
+
expect(errsStale.some((e) => e.includes("--force to regenerate"))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("rejects a non-kebab appName via the underlying scaffold validation", async () => {
|
|
97
|
+
const { out, errs } = makeOut();
|
|
98
|
+
const code = await runInitDeployCli({ argv: ["--app", "MyApp"], cwd: tmp, out });
|
|
99
|
+
expect(code).toBe(1);
|
|
100
|
+
expect(errs.some((e) => e.includes("kebab-case"))).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("rejects an invalid package.json#kumiko.deploy config with exit 1", async () => {
|
|
104
|
+
writeFileSync(
|
|
105
|
+
join(tmp, "package.json"),
|
|
106
|
+
JSON.stringify({ name: "myapp", kumiko: { deploy: { stackNetwork: "foo" } } }),
|
|
107
|
+
);
|
|
108
|
+
const { out, errs } = makeOut();
|
|
109
|
+
const code = await runInitDeployCli({ argv: ["--app", "myapp"], cwd: tmp, out });
|
|
110
|
+
expect(code).toBe(1);
|
|
111
|
+
expect(errs.some((e) => e.includes("stackNetwork"))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -2,7 +2,7 @@ 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";
|
|
5
|
-
import { render, scaffoldDeploy } from "../scaffold-deploy";
|
|
5
|
+
import { checkDeployDrift, render, scaffoldDeploy } from "../scaffold-deploy";
|
|
6
6
|
|
|
7
7
|
describe("scaffoldDeploy", () => {
|
|
8
8
|
let tmp: string;
|
|
@@ -212,13 +212,13 @@ describe("scaffoldDeploy", () => {
|
|
|
212
212
|
const result = scaffoldDeploy({ appName: "ghapp", destination: tmp });
|
|
213
213
|
expect(result.detected.hasPrivateGhPackages).toBe(true);
|
|
214
214
|
const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
|
|
215
|
-
expect(df).toContain("ARG
|
|
216
|
-
expect(df).toContain("ARG
|
|
215
|
+
expect(df).toContain("ARG NPM_AUTH_TOKEN=");
|
|
216
|
+
expect(df).toContain("ARG NPM_AUTH_TOKEN\n");
|
|
217
217
|
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell variable expansion, not a JS template
|
|
218
|
-
expect(df).toContain("ENV GITHUB_TOKEN=${
|
|
218
|
+
expect(df).toContain("ENV GITHUB_TOKEN=${NPM_AUTH_TOKEN}");
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
-
it("skips
|
|
221
|
+
it("skips NPM_AUTH_TOKEN blocks when only public @cosmicdrift/* deps are present", () => {
|
|
222
222
|
writeFileSync(
|
|
223
223
|
join(tmp, "package.json"),
|
|
224
224
|
JSON.stringify({
|
|
@@ -232,21 +232,233 @@ describe("scaffoldDeploy", () => {
|
|
|
232
232
|
const result = scaffoldDeploy({ appName: "publicapp", destination: tmp });
|
|
233
233
|
expect(result.detected.hasPrivateGhPackages).toBe(false);
|
|
234
234
|
const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
|
|
235
|
-
expect(df).not.toContain("ARG
|
|
235
|
+
expect(df).not.toContain("ARG NPM_AUTH_TOKEN");
|
|
236
236
|
expect(df).not.toContain("ENV GITHUB_TOKEN");
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
it("
|
|
239
|
+
it("manifests app with bunfig.toml: NPM_AUTH_TOKEN wiring + manifests-first COPY", () => {
|
|
240
|
+
writeFileSync(
|
|
241
|
+
join(tmp, "package.json"),
|
|
242
|
+
JSON.stringify({
|
|
243
|
+
name: "manifestapp",
|
|
244
|
+
dependencies: { "@cosmicdriftgamestudio/kumiko-ai-foundation": "^0.2.0" },
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
writeFileSync(join(tmp, "bunfig.toml"), "[install.scopes]\n");
|
|
248
|
+
const result = scaffoldDeploy({ appName: "manifestapp", destination: tmp });
|
|
249
|
+
expect(result.detected.installFromFullTree).toBe(false);
|
|
250
|
+
expect(result.detected.registryConfigFiles).toEqual(["bunfig.toml"]);
|
|
251
|
+
const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
|
|
252
|
+
expect(df).toContain("ARG NPM_AUTH_TOKEN=");
|
|
253
|
+
expect(df).toContain("ARG NPM_AUTH_TOKEN\n");
|
|
254
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell variable expansion, not a JS template
|
|
255
|
+
expect(df).toContain("ENV GITHUB_TOKEN=${NPM_AUTH_TOKEN}");
|
|
256
|
+
expect(df).toContain(
|
|
257
|
+
"COPY package.json bun.lock bunfig.toml ./\nRUN bun install --frozen-lockfile",
|
|
258
|
+
);
|
|
259
|
+
expect(df).toMatch(
|
|
260
|
+
/FROM .* AS runtime\n(?:#[^\n]*\n)*ARG BUILD_VERSION=dev\nARG BUILD_TIME=unknown/,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it(".npmrc-only app copies .npmrc before install", () => {
|
|
265
|
+
writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "npmrcapp" }));
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(tmp, ".npmrc"),
|
|
268
|
+
"@cosmicdriftgamestudio:registry=https://npm.pkg.github.com\n",
|
|
269
|
+
);
|
|
270
|
+
const result = scaffoldDeploy({ appName: "npmrcapp", destination: tmp });
|
|
271
|
+
expect(result.detected.registryConfigFiles).toEqual([".npmrc"]);
|
|
272
|
+
const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
|
|
273
|
+
expect(df).toContain("COPY package.json bun.lock .npmrc ./");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("a workspaces app renders COPY . . before install (full-tree)", () => {
|
|
277
|
+
writeFileSync(
|
|
278
|
+
join(tmp, "package.json"),
|
|
279
|
+
JSON.stringify({ name: "wsapp", workspaces: ["packages/*"] }),
|
|
280
|
+
);
|
|
281
|
+
const result = scaffoldDeploy({ appName: "wsapp", destination: tmp });
|
|
282
|
+
expect(result.detected.installFromFullTree).toBe(true);
|
|
283
|
+
const df = readFileSync(join(tmp, "deploy", "Dockerfile"), "utf-8");
|
|
284
|
+
expect(df).toContain("COPY . .\nRUN bun install --frozen-lockfile\nRUN bun run build");
|
|
285
|
+
expect(df).not.toContain("COPY package.json bun.lock");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("a file:/workspace:/link: dependency spec triggers full-tree install without a workspaces field", () => {
|
|
289
|
+
writeFileSync(
|
|
290
|
+
join(tmp, "package.json"),
|
|
291
|
+
JSON.stringify({ name: "localdepapp", dependencies: { "@app/define": "file:./.kumiko" } }),
|
|
292
|
+
);
|
|
293
|
+
const result = scaffoldDeploy({ appName: "localdepapp", destination: tmp });
|
|
294
|
+
expect(result.detected.installFromFullTree).toBe(true);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("kumiko.deploy config", () => {
|
|
299
|
+
it("no kumiko.deploy → migrate-step uses appName as db user and the discover block", () => {
|
|
300
|
+
scaffoldDeploy({ appName: "plainapp", destination: tmp });
|
|
301
|
+
const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
|
|
302
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
|
|
303
|
+
expect(migrate).toContain("postgresql://plainapp:${DB_PASSWORD}@db:5432/plainapp");
|
|
304
|
+
expect(migrate).toContain("docker network ls");
|
|
305
|
+
expect(migrate).not.toContain("basename");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('dbUser "kumiko" substitutes only the DB-URL user, db name stays appName', () => {
|
|
309
|
+
writeFileSync(
|
|
310
|
+
join(tmp, "package.json"),
|
|
311
|
+
JSON.stringify({ name: "dbuserapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
|
|
312
|
+
);
|
|
313
|
+
scaffoldDeploy({ appName: "dbuserapp", destination: tmp });
|
|
314
|
+
const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
|
|
315
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
|
|
316
|
+
expect(migrate).toContain("postgresql://kumiko:${DB_PASSWORD}@db:5432/dbuserapp");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('stackNetwork "directory" renders the exact-name block, not the discover block', () => {
|
|
320
|
+
writeFileSync(
|
|
321
|
+
join(tmp, "package.json"),
|
|
322
|
+
JSON.stringify({ name: "dirapp", kumiko: { deploy: { stackNetwork: "directory" } } }),
|
|
323
|
+
);
|
|
324
|
+
scaffoldDeploy({ appName: "dirapp", destination: tmp });
|
|
325
|
+
const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
|
|
326
|
+
expect(migrate).toContain('STACK_NETWORK="$(basename "$PWD")_stack"');
|
|
327
|
+
expect(migrate).toContain("docker network inspect");
|
|
328
|
+
expect(migrate).not.toContain("docker network ls");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it.each(["a;rm -rf /", "$(id)", "a b", ""])(
|
|
332
|
+
"rejects invalid dbUser %j, naming the field",
|
|
333
|
+
(dbUser) => {
|
|
334
|
+
writeFileSync(
|
|
335
|
+
join(tmp, "package.json"),
|
|
336
|
+
JSON.stringify({ name: "invaliddbuser", kumiko: { deploy: { dbUser } } }),
|
|
337
|
+
);
|
|
338
|
+
expect(() => scaffoldDeploy({ appName: "invaliddbuser", destination: tmp })).toThrow(
|
|
339
|
+
/dbUser/,
|
|
340
|
+
);
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
it("rejects an invalid stackNetwork value", () => {
|
|
345
|
+
writeFileSync(
|
|
346
|
+
join(tmp, "package.json"),
|
|
347
|
+
JSON.stringify({ name: "invalidnet", kumiko: { deploy: { stackNetwork: "foo" } } }),
|
|
348
|
+
);
|
|
349
|
+
expect(() => scaffoldDeploy({ appName: "invalidnet", destination: tmp })).toThrow(
|
|
350
|
+
/stackNetwork/,
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("rejects an unknown key under kumiko.deploy (strict)", () => {
|
|
355
|
+
writeFileSync(
|
|
356
|
+
join(tmp, "package.json"),
|
|
357
|
+
JSON.stringify({ name: "unknownkey", kumiko: { deploy: { dbHost: "elsewhere" } } }),
|
|
358
|
+
);
|
|
359
|
+
expect(() => scaffoldDeploy({ appName: "unknownkey", destination: tmp })).toThrow(/dbHost/);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("checkDeployDrift", () => {
|
|
364
|
+
it("reports missing when no deploy files exist yet", () => {
|
|
365
|
+
const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
|
|
366
|
+
expect(result.drifted).toHaveLength(3);
|
|
367
|
+
expect(result.drifted.every((d) => d.reason === "missing")).toBe(true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("reports differs when an on-disk file no longer matches the rendered content", () => {
|
|
371
|
+
scaffoldDeploy({ appName: "driftapp", destination: tmp });
|
|
372
|
+
writeFileSync(join(tmp, "deploy", "Dockerfile"), "# hand-edited, drifted");
|
|
373
|
+
const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
|
|
374
|
+
expect(result.drifted).toEqual([
|
|
375
|
+
{ path: join(tmp, "deploy", "Dockerfile"), reason: "differs" },
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("reports no drift right after a fresh scaffold", () => {
|
|
380
|
+
scaffoldDeploy({ appName: "driftapp", destination: tmp });
|
|
381
|
+
const result = checkDeployDrift({ appName: "driftapp", destination: tmp });
|
|
382
|
+
expect(result.drifted).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("reports no drift for a scaffold rendered with a kumiko.deploy config", () => {
|
|
386
|
+
writeFileSync(
|
|
387
|
+
join(tmp, "package.json"),
|
|
388
|
+
JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
|
|
389
|
+
);
|
|
390
|
+
scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
|
|
391
|
+
const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
|
|
392
|
+
expect(result.drifted).toHaveLength(0);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("reports differs on migrate-step.sh after package.json's dbUser changes", () => {
|
|
396
|
+
writeFileSync(
|
|
397
|
+
join(tmp, "package.json"),
|
|
398
|
+
JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "kumiko" } } }),
|
|
399
|
+
);
|
|
400
|
+
scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
|
|
401
|
+
writeFileSync(
|
|
402
|
+
join(tmp, "package.json"),
|
|
403
|
+
JSON.stringify({ name: "driftconfigapp", kumiko: { deploy: { dbUser: "otheruser" } } }),
|
|
404
|
+
);
|
|
405
|
+
const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
|
|
406
|
+
expect(result.drifted).toEqual([
|
|
407
|
+
{ path: join(tmp, "deploy", "migrate-step.sh"), reason: "differs" },
|
|
408
|
+
]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("reports differs when the file on disk uses defaults but package.json now says directory", () => {
|
|
412
|
+
scaffoldDeploy({ appName: "driftconfigapp", destination: tmp });
|
|
413
|
+
writeFileSync(
|
|
414
|
+
join(tmp, "package.json"),
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
name: "driftconfigapp",
|
|
417
|
+
kumiko: { deploy: { stackNetwork: "directory" } },
|
|
418
|
+
}),
|
|
419
|
+
);
|
|
420
|
+
const result = checkDeployDrift({ appName: "driftconfigapp", destination: tmp });
|
|
421
|
+
expect(result.drifted).toEqual([
|
|
422
|
+
{ path: join(tmp, "deploy", "migrate-step.sh"), reason: "differs" },
|
|
423
|
+
]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe("malformed package.json", () => {
|
|
428
|
+
it("warns + defaults to no private deps and manifests-first install (mis-detection is visible)", () => {
|
|
240
429
|
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
241
430
|
try {
|
|
242
431
|
writeFileSync(join(tmp, "package.json"), "{ this is not json");
|
|
243
432
|
const result = scaffoldDeploy({ appName: "broken", destination: tmp });
|
|
244
433
|
expect(result.detected.hasPrivateGhPackages).toBe(false);
|
|
434
|
+
expect(result.detected.installFromFullTree).toBe(false);
|
|
245
435
|
expect(warn).toHaveBeenCalledTimes(1);
|
|
246
436
|
expect(warn.mock.calls[0]?.[0]).toContain("is not valid JSON");
|
|
247
437
|
} finally {
|
|
248
438
|
warn.mockRestore();
|
|
249
439
|
}
|
|
250
440
|
});
|
|
441
|
+
|
|
442
|
+
it("keeps a valid kumiko.deploy config even when dependencies has an unexpected shape", () => {
|
|
443
|
+
const warn = spyOn(console, "warn").mockImplementation(() => {});
|
|
444
|
+
try {
|
|
445
|
+
writeFileSync(
|
|
446
|
+
join(tmp, "package.json"),
|
|
447
|
+
JSON.stringify({
|
|
448
|
+
name: "shapeissue",
|
|
449
|
+
dependencies: "not-an-object",
|
|
450
|
+
kumiko: { deploy: { dbUser: "kumiko" } },
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
const result = scaffoldDeploy({ appName: "shapeissue", destination: tmp });
|
|
454
|
+
expect(result.detected.hasPrivateGhPackages).toBe(false);
|
|
455
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
456
|
+
const migrate = readFileSync(join(tmp, "deploy", "migrate-step.sh"), "utf-8");
|
|
457
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: shell-substitution literal, not a JS template
|
|
458
|
+
expect(migrate).toContain("postgresql://kumiko:${DB_PASSWORD}@db:5432/shapeissue");
|
|
459
|
+
} finally {
|
|
460
|
+
warn.mockRestore();
|
|
461
|
+
}
|
|
462
|
+
});
|
|
251
463
|
});
|
|
252
464
|
});
|
package/src/build.ts
CHANGED
|
@@ -9,10 +9,12 @@ export {
|
|
|
9
9
|
type BuildResult,
|
|
10
10
|
buildProdBundle,
|
|
11
11
|
type ClientEntry,
|
|
12
|
-
|
|
12
|
+
type ClientEntryDeclaration,
|
|
13
13
|
discoverHtmlTemplate,
|
|
14
14
|
formatBuildResult,
|
|
15
15
|
injectAssetTags,
|
|
16
|
+
readClientEntriesConfig,
|
|
17
|
+
resolveClientEntries,
|
|
16
18
|
} from "@cosmicdrift/kumiko-server-runtime/build-prod-bundle";
|
|
17
19
|
|
|
18
20
|
export {
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.323.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Deploy Dockerfile template: NPM_AUTH_TOKEN registry auth, manifest-first or full-tree install by detected layout, BUILD_VERSION/BUILD_TIME in the runtime stage, TZ=UTC; new kumiko-init-deploy bin with --force and a --check drift check",
|
|
6
|
+
"migration": "The scaffolded `deploy/Dockerfile` now declares `ARG NPM_AUTH_TOKEN`\n(global + re-declared in the build stage) instead of `ARG GITHUB_TOKEN`,\nand exports `ENV GITHUB_TOKEN=${NPM_AUTH_TOKEN}` in the build stage —\nbunfig.toml/.npmrc still read `$GITHUB_TOKEN`, only the build-arg name\npassed by CI changes. Apps that already generated `deploy/Dockerfile`\nfrom an older template keep working unchanged; re-run\n`kumiko-init-deploy --force` (or `kumiko init-deploy --force` from the\nmonorepo) to pick up the new wiring, the runtime stage's\n`ARG BUILD_VERSION=dev`/`ARG BUILD_TIME=unknown` re-declaration (without\nit the runtime `ENV BUILD_VERSION`/`ENV BUILD_TIME` stayed empty despite\na correct `--build-arg`), and `ENV TZ=UTC`.\n\nThe install step now copies manifests first (`package.json`, `bun.lock`,\nand whichever of `bunfig.toml`/`.npmrc` exist) before `bun install`,\nunless the app's `package.json` has a `workspaces` field or a\n`file:`/`workspace:`/`link:` dependency spec, in which case it still\ndoes `COPY . .` first (bun needs the whole tree to resolve the\nlockfile).\n\n`scaffoldDeploy`/`ScaffoldDeployOptions`/`ScaffoldDeployResult` keep\ntheir existing shape and behavior. New: `renderDeployFiles` (pure,\nno writes) and `checkDeployDrift` (read-only, reports missing/differing\nfiles) are exported alongside `scaffoldDeploy`, which now calls\n`renderDeployFiles` internally. `ScaffoldDeployDetected` gained\n`installFromFullTree` and `registryConfigFiles`.\n\nNew `kumiko-init-deploy` bin (and `runInitDeployCli` export) — same\nflags as `kumiko init-deploy` (`--app`, `--port`, `--github-org`,\n`--out`, `--force`), plus `--check` (drift check, exit 1 on any\nmissing/differing file, does not write) and defaulting `--app` from\n`package.json`'s `name` (scope stripped) when omitted. `--check` and\n`--force` are mutually exclusive (exit 2). The monorepo's\n`kumiko init-deploy` command now delegates to `runInitDeployCli`\ninstead of duplicating the CLI logic."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.323.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "migrate-step.sh reads package.json#kumiko.deploy (dbUser, stackNetwork \"discover\" | \"directory\"); defaults unchanged, invalid values fail the render and the drift check",
|
|
12
|
+
"migration": "No action needed: without `kumiko.deploy` the scaffolded\n`deploy/migrate-step.sh` renders byte-identical to before. Apps whose\ndatabase user is not the app name set\n`\"kumiko\": { \"deploy\": { \"dbUser\": \"<user>\" } }`; apps that want the\nexact `<dirname>_stack` network instead of the `docker network ls`\nheuristic set `\"stackNetwork\": \"directory\"`. Both `kumiko-init-deploy\n--force` and `--check` read the same config, so the drift check needs\nno extra flags. `ScaffoldDeployDetected` gained `dbUser` and\n`stackNetwork`."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.323.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "runDevApp wires auth-email-password's new invite-info query into invite.infoHandler",
|
|
18
|
+
"detail": "Same wiring as runProdApp, for the dev server.",
|
|
19
|
+
"migration": "No action needed: purely additive wiring, no option changes."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"version": "0.323.0",
|
|
23
|
+
"type": "breaking",
|
|
24
|
+
"title": "kumiko-build reads client entries from package.json \"kumiko.clientEntries\"/\"kumiko.clientEntry\" instead of inferring them from filenames",
|
|
25
|
+
"detail": "`kumiko-build` (and the `kumiko build` command) now call\n`readClientEntriesConfig(cwd)` and pass the result into\n`buildProdBundle`, instead of relying on `discoverClientEntries`'\nfilename-based multi-entry discovery. `@cosmicdrift/kumiko-dev-server/build`\nre-exports `resolveClientEntries`, `readClientEntriesConfig`, and\n`ClientEntryDeclaration` in place of `discoverClientEntries`.",
|
|
26
|
+
"migration": "See the `@cosmicdrift/kumiko-server-runtime` changelog entry above for\nthe required package.json changes — the dev-server change is the CLI\nwiring for the same underlying behavior."
|
|
27
|
+
},
|
|
2
28
|
{
|
|
3
29
|
"version": "0.321.0",
|
|
4
30
|
"type": "improvement",
|
|
@@ -85,18 +85,20 @@ const logInfo = (msg: string): void => console.log(msg);
|
|
|
85
85
|
// biome-ignore lint/suspicious/noConsole: dev-server error logging
|
|
86
86
|
const logError = (...args: unknown[]): void => console.error(...args);
|
|
87
87
|
|
|
88
|
-
/** Multi-
|
|
89
|
-
*
|
|
88
|
+
/** Multi-entry mode for apps that ship several separate bundles (e.g.
|
|
89
|
+
* publicstatus: `admin.<base>` loads the admin UI, otherwise the public
|
|
90
|
+
* page).
|
|
90
91
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
92
|
+
* kumiko-build reads the same entry list from package.json
|
|
93
|
+
* `kumiko.clientEntries` and serves `/client-<name>.js` per HTTP — the
|
|
94
|
+
* dev server and the prod build stay in sync via that shared declaration,
|
|
95
|
+
* not a filename convention. Mutually exclusive with `clientEntry`.
|
|
96
|
+
* Multi-entry apps MUST also set `hostDispatch` — otherwise the server
|
|
97
|
+
* has no way to know which HTML to serve. */
|
|
96
98
|
export type DevClientEntry = {
|
|
97
|
-
/** Logical
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
99
|
+
/** Logical name. Should match the corresponding package.json
|
|
100
|
+
* `kumiko.clientEntries[].name` so dev and prod produce identical
|
|
101
|
+
* asset URLs (`/client-<name>.js`). */
|
|
100
102
|
readonly name: string;
|
|
101
103
|
/** Absoluter Pfad zur Browser-Entry-Datei. */
|
|
102
104
|
readonly sourceFile: string;
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,8 @@ export type {
|
|
|
68
68
|
} from "./few-shot-corpus";
|
|
69
69
|
export { buildFewShotCorpus, pathToId } from "./few-shot-corpus";
|
|
70
70
|
export { resolveFrameworkVersion } from "./framework-version";
|
|
71
|
+
export type { CliOutput, RunInitDeployCliOptions } from "./init-deploy-cli";
|
|
72
|
+
export { runInitDeployCli } from "./init-deploy-cli";
|
|
71
73
|
export type { RunDevAppAuthOptions, RunDevAppOptions, SeedFn } from "./run-dev-app";
|
|
72
74
|
export { runDevApp } from "./run-dev-app";
|
|
73
75
|
export type {
|
|
@@ -82,11 +84,17 @@ export type {
|
|
|
82
84
|
} from "./scaffold-app-feature";
|
|
83
85
|
export { runConfigPathForApp, scaffoldAppFeature } from "./scaffold-app-feature";
|
|
84
86
|
export type {
|
|
87
|
+
CheckDeployDriftResult,
|
|
88
|
+
DeployDriftEntry,
|
|
89
|
+
RenderDeployFilesOptions,
|
|
90
|
+
RenderDeployFilesResult,
|
|
91
|
+
RenderedDeployFile,
|
|
92
|
+
ScaffoldDeployDetected,
|
|
85
93
|
ScaffoldDeployOptions,
|
|
86
94
|
ScaffoldDeployResult,
|
|
87
95
|
ScaffoldedFile,
|
|
88
96
|
} from "./scaffold-deploy";
|
|
89
|
-
export { scaffoldDeploy } from "./scaffold-deploy";
|
|
97
|
+
export { checkDeployDrift, renderDeployFiles, scaffoldDeploy } from "./scaffold-deploy";
|
|
90
98
|
export type { ScaffoldFeatureOptions, ScaffoldFeatureResult } from "./scaffold-feature";
|
|
91
99
|
export { scaffoldFeature } from "./scaffold-feature";
|
|
92
100
|
export {
|