@cosmicdrift/kumiko-dev-server 0.57.2 → 0.59.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.
|
|
3
|
+
"version": "0.59.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>",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"kumiko-schema-check": "./bin/kumiko-schema-check.ts"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
50
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
49
|
+
"@cosmicdrift/kumiko-bundled-features": "0.57.2",
|
|
50
|
+
"@cosmicdrift/kumiko-framework": "0.57.2",
|
|
51
51
|
"ts-morph": "^28.0.0"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
@@ -13,6 +13,7 @@ import { tmpdir } from "node:os";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import {
|
|
15
15
|
type ClientEntry,
|
|
16
|
+
computeBuildId,
|
|
16
17
|
discoverClientEntries,
|
|
17
18
|
discoverClientEntry,
|
|
18
19
|
discoverHtmlTemplate,
|
|
@@ -260,3 +261,51 @@ describe("build-prod-bundle/injectAssetTags", () => {
|
|
|
260
261
|
);
|
|
261
262
|
});
|
|
262
263
|
});
|
|
264
|
+
|
|
265
|
+
describe("build-prod-bundle/computeBuildId", () => {
|
|
266
|
+
test("identisches Manifest → gleiche id", () => {
|
|
267
|
+
const manifest = {
|
|
268
|
+
"client.js": "/assets/client-abc.js",
|
|
269
|
+
"styles.css": "/assets/styles-xyz.css",
|
|
270
|
+
};
|
|
271
|
+
expect(computeBuildId(manifest)).toBe(computeBuildId({ ...manifest }));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("Key-Reihenfolge egal — nur die URL-Werte zählen", () => {
|
|
275
|
+
const a = { "client.js": "/assets/client-abc.js", "styles.css": "/assets/styles-xyz.css" };
|
|
276
|
+
const b = { "styles.css": "/assets/styles-xyz.css", "client.js": "/assets/client-abc.js" };
|
|
277
|
+
expect(computeBuildId(a)).toBe(computeBuildId(b));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("geändertes Asset (neuer Hash) → andere id", () => {
|
|
281
|
+
const before = { "client.js": "/assets/client-abc.js" };
|
|
282
|
+
const after = { "client.js": "/assets/client-def.js" };
|
|
283
|
+
expect(computeBuildId(before)).not.toBe(computeBuildId(after));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("12 Hex-Zeichen", () => {
|
|
287
|
+
expect(computeBuildId({ "client.js": "/assets/client-abc.js" })).toMatch(/^[0-9a-f]{12}$/);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe("build-prod-bundle/injectAssetTags build-info", () => {
|
|
292
|
+
test("bäckt window.__KUMIKO_BUILD__ vor </head> wenn buildInfo gesetzt", () => {
|
|
293
|
+
const html = `<html><head><title>x</title></head><body><script type="module" src="/client.js"></script></body></html>`;
|
|
294
|
+
const result = injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry(), {
|
|
295
|
+
id: "deadbeef0000",
|
|
296
|
+
builtAt: "2026-06-18T12:00:00.000Z",
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(result).toContain(
|
|
300
|
+
'window.__KUMIKO_BUILD__={"id":"deadbeef0000","builtAt":"2026-06-18T12:00:00.000Z"}',
|
|
301
|
+
);
|
|
302
|
+
expect(result.indexOf("__KUMIKO_BUILD__")).toBeLessThan(result.indexOf("</head>"));
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("ohne buildInfo kein __KUMIKO_BUILD__-Script", () => {
|
|
306
|
+
const html = `<html><head></head><body><script type="module" src="/client.js"></script></body></html>`;
|
|
307
|
+
const result = injectAssetTags(html, { "client.js": "/assets/client-abc.js" }, clientEntry());
|
|
308
|
+
|
|
309
|
+
expect(result).not.toContain("__KUMIKO_BUILD__");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// Strategie (siehe run-prod-app.ts):
|
|
5
5
|
// /assets/* → max-age=31536000, immutable
|
|
6
6
|
// /, /index.html → no-cache, must-revalidate
|
|
7
|
-
// /manifest.json, /sw.js
|
|
7
|
+
// /manifest.json, /sw.js,
|
|
8
|
+
// /build-info.json → no-cache
|
|
8
9
|
// alles andere → kein expliziter Header
|
|
9
10
|
|
|
10
11
|
import { describe, expect, test } from "bun:test";
|
|
@@ -53,6 +54,12 @@ describe("cacheHeadersFor", () => {
|
|
|
53
54
|
});
|
|
54
55
|
});
|
|
55
56
|
|
|
57
|
+
test("/build-info.json → no-cache (sonst pollt UpdateChecker eine veraltete id)", () => {
|
|
58
|
+
expect(cacheHeadersFor("/build-info.json")).toEqual({
|
|
59
|
+
"cache-control": "no-cache",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
56
63
|
test("public-folder file (favicon) → kein expliziter Header", () => {
|
|
57
64
|
expect(cacheHeadersFor("/favicon.ico")).toEqual({});
|
|
58
65
|
});
|
package/src/build-prod-bundle.ts
CHANGED
|
@@ -71,6 +71,20 @@ export type BuildResult = {
|
|
|
71
71
|
* { "client.js": "/assets/client-a3f2.js",
|
|
72
72
|
* "styles.css": "/assets/styles-9b4c.css" } */
|
|
73
73
|
readonly manifest: BuildManifest;
|
|
74
|
+
/** Build-Identität (Hash über die Asset-URLs) + lesbarer Zeitstempel.
|
|
75
|
+
* undefined bei vanilla public-only-Builds (kein Bundle). */
|
|
76
|
+
readonly buildInfo?: BuildInfo;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** In index.html gebacken (`window.__KUMIKO_BUILD__`) UND als
|
|
80
|
+
* dist/build-info.json geschrieben. Der UpdateChecker pollt build-info.json
|
|
81
|
+
* und vergleicht `id` gegen den geladenen Stand → Reload-Banner bei Drift. */
|
|
82
|
+
export type BuildInfo = {
|
|
83
|
+
/** Hash über die sortierten (content-gehashten) Asset-URLs. Ändert sich
|
|
84
|
+
* gdw. sich ein Asset ändert — selbsttragend, kein Env-Var/Dockerfile. */
|
|
85
|
+
readonly id: string;
|
|
86
|
+
/** ISO-Zeitstempel des Builds, lesbare Anzeige-Version (ersetzt den rohen sha). */
|
|
87
|
+
readonly builtAt: string;
|
|
74
88
|
};
|
|
75
89
|
|
|
76
90
|
// Default-HTML wird nur genutzt wenn der App-Author KEIN index.html liefert.
|
|
@@ -166,6 +180,17 @@ export async function buildProdBundle(options: BuildProdBundleOptions = {}): Pro
|
|
|
166
180
|
await copyPublicFolder(publicDir, outDir, templateBasenames);
|
|
167
181
|
}
|
|
168
182
|
|
|
183
|
+
// 5b. Build-Identität: Hash über die content-gehashten Asset-URLs +
|
|
184
|
+
// lesbarer Zeitstempel. Wird in index.html gebacken und als
|
|
185
|
+
// build-info.json geschrieben (Update-Awareness, default an).
|
|
186
|
+
// ponytail: id hängt nur an Assets — ein reiner Server-Deploy ohne
|
|
187
|
+
// Client-Bundle-Change behält die id → kein Banner (die UI ist dann
|
|
188
|
+
// nicht stale). Bei Bedarf an Server-Versionierung koppeln.
|
|
189
|
+
const buildInfo: BuildInfo | undefined =
|
|
190
|
+
Object.keys(manifest).length > 0
|
|
191
|
+
? { id: computeBuildId(manifest), builtAt: new Date().toISOString() }
|
|
192
|
+
: undefined;
|
|
193
|
+
|
|
169
194
|
// 6. HTML pro Entry rendern. Convention: ein HTML-File pro Client-Entry,
|
|
170
195
|
// jede mit ihrem eigenen Script-Tag. Server (runProdApp.hostDispatch)
|
|
171
196
|
// serviert je nach Host das passende File.
|
|
@@ -176,16 +201,24 @@ export async function buildProdBundle(options: BuildProdBundleOptions = {}): Pro
|
|
|
176
201
|
for (const entry of clientEntries) {
|
|
177
202
|
const templatePath = resolve(cwd, entry.htmlPath);
|
|
178
203
|
const templateExists = existsSync(templatePath);
|
|
179
|
-
const html = await renderHtml(
|
|
204
|
+
const html = await renderHtml(
|
|
205
|
+
templateExists ? templatePath : undefined,
|
|
206
|
+
manifest,
|
|
207
|
+
entry,
|
|
208
|
+
buildInfo,
|
|
209
|
+
);
|
|
180
210
|
const outFile = basenameOf(entry.htmlPath);
|
|
181
211
|
await writeFile(join(outDir, outFile), html);
|
|
182
212
|
}
|
|
183
213
|
}
|
|
184
214
|
|
|
185
|
-
// 7. Manifest.
|
|
215
|
+
// 7. Manifest + Build-Info.
|
|
186
216
|
await writeFile(join(outDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
217
|
+
if (buildInfo) {
|
|
218
|
+
await writeFile(join(outDir, "build-info.json"), `${JSON.stringify(buildInfo)}\n`);
|
|
219
|
+
}
|
|
187
220
|
|
|
188
|
-
return { outDir, manifest };
|
|
221
|
+
return { outDir, manifest, ...(buildInfo && { buildInfo }) };
|
|
189
222
|
}
|
|
190
223
|
|
|
191
224
|
// ---------------------------------------------------------------------------
|
|
@@ -493,6 +526,7 @@ async function renderHtml(
|
|
|
493
526
|
templatePath: string | undefined,
|
|
494
527
|
manifest: BuildManifest,
|
|
495
528
|
entry: ClientEntry,
|
|
529
|
+
buildInfo: BuildInfo | undefined,
|
|
496
530
|
): Promise<string> {
|
|
497
531
|
// Edge-Case: kein eigenes HTML-Template + Bun.build oder Tailwind hat
|
|
498
532
|
// Output produziert. DEFAULT_HTML hat keine Placeholder (vanilla
|
|
@@ -502,7 +536,7 @@ async function renderHtml(
|
|
|
502
536
|
throw new Error(buildMissingTemplateError(manifest, entry));
|
|
503
537
|
}
|
|
504
538
|
const template = templatePath ? await readFile(templatePath, "utf8") : DEFAULT_HTML;
|
|
505
|
-
return injectAssetTags(template, manifest, entry);
|
|
539
|
+
return injectAssetTags(template, manifest, entry, buildInfo);
|
|
506
540
|
}
|
|
507
541
|
|
|
508
542
|
function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry): string {
|
|
@@ -545,9 +579,21 @@ function buildMissingTemplateError(manifest: BuildManifest, entry: ClientEntry):
|
|
|
545
579
|
// Fehlt ein erwarteter Tag, wirft der Build einen Fehler mit dem exakten
|
|
546
580
|
// Snippet zum Reinkopieren — kein silent injection mehr, weil das den
|
|
547
581
|
// Diff zwischen Dev- und Prod-HTML unsichtbar macht.
|
|
548
|
-
export function injectAssetTags(
|
|
582
|
+
export function injectAssetTags(
|
|
583
|
+
html: string,
|
|
584
|
+
manifest: BuildManifest,
|
|
585
|
+
entry: ClientEntry,
|
|
586
|
+
buildInfo?: BuildInfo,
|
|
587
|
+
): string {
|
|
549
588
|
let result = html;
|
|
550
589
|
|
|
590
|
+
// Build-Stand vor </head> backen. id/builtAt sind Hex bzw. ISO → kein
|
|
591
|
+
// </script>-Escaping nötig. Ohne </head> (z.B. Fragment) still skip.
|
|
592
|
+
if (buildInfo && result.includes("</head>")) {
|
|
593
|
+
const tag = `<script>window.__KUMIKO_BUILD__=${JSON.stringify(buildInfo)};</script>`;
|
|
594
|
+
result = result.replace("</head>", ` ${tag}\n </head>`);
|
|
595
|
+
}
|
|
596
|
+
|
|
551
597
|
const cssUrl = manifest["styles.css"];
|
|
552
598
|
if (cssUrl && !result.includes(cssUrl)) {
|
|
553
599
|
const placeholder = /<link\s+rel="stylesheet"\s+href="\/styles\.css"\s*\/?>/.exec(result);
|
|
@@ -617,6 +663,17 @@ function shortHash(content: string | Uint8Array): string {
|
|
|
617
663
|
return createHash("sha256").update(content).digest("hex").slice(0, 8);
|
|
618
664
|
}
|
|
619
665
|
|
|
666
|
+
// Build-Identität aus den content-gehashten Asset-URLs. Sortiert →
|
|
667
|
+
// deterministisch (Manifest-Key-Reihenfolge egal). Identischer Source →
|
|
668
|
+
// gleiche id, geändertes Asset → andere id.
|
|
669
|
+
// @internal — exported nur für Unit-Tests.
|
|
670
|
+
export function computeBuildId(manifest: BuildManifest): string {
|
|
671
|
+
return createHash("sha256")
|
|
672
|
+
.update(Object.values(manifest).sort().join("\n"))
|
|
673
|
+
.digest("hex")
|
|
674
|
+
.slice(0, 12);
|
|
675
|
+
}
|
|
676
|
+
|
|
620
677
|
// ---------------------------------------------------------------------------
|
|
621
678
|
// CLI output
|
|
622
679
|
// ---------------------------------------------------------------------------
|
package/src/run-prod-app.ts
CHANGED
|
@@ -1161,7 +1161,14 @@ export function cacheHeadersFor(pathname: string): Record<string, string> {
|
|
|
1161
1161
|
if (pathname === "/" || pathname === "/index.html") {
|
|
1162
1162
|
return { "cache-control": "no-cache, must-revalidate" };
|
|
1163
1163
|
}
|
|
1164
|
-
if (
|
|
1164
|
+
if (
|
|
1165
|
+
pathname === "/manifest.json" ||
|
|
1166
|
+
pathname === "/sw.js" ||
|
|
1167
|
+
// ponytail: build-info.json ist statisch — kein /api/version-Endpoint
|
|
1168
|
+
// nötig, der Disk-Fallback serviert sie. no-cache, sonst pollt der
|
|
1169
|
+
// UpdateChecker eine veraltete id.
|
|
1170
|
+
pathname === "/build-info.json"
|
|
1171
|
+
) {
|
|
1165
1172
|
return { "cache-control": "no-cache" };
|
|
1166
1173
|
}
|
|
1167
1174
|
return {};
|