@cosmicdrift/kumiko-dev-server 0.152.0 → 0.154.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 +5 -8
- package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
- package/src/__tests__/compose-stacks.test.ts +1 -1
- package/src/__tests__/discover-format.test.ts +1 -1
- package/src/__tests__/env-schema.integration.test.ts +1 -1
- package/src/__tests__/walkthrough.integration.test.ts +1 -1
- package/src/build.ts +1 -1
- package/src/create-kumiko-server.ts +12 -6
- package/src/index.ts +4 -12
- package/src/run-dev-app.ts +12 -8
- package/src/scaffold-app.ts +9 -4
- package/src/schema-apply.ts +4 -1
- package/src/schema-check-core.ts +1 -1
- package/src/setup-test-stack-from-features.ts +4 -1
- package/src/__tests__/boot-extra-context.test.ts +0 -140
- package/src/__tests__/build-prod-bundle.test.ts +0 -278
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
- package/src/__tests__/compose-features.test.ts +0 -128
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/inject-schema.test.ts +0 -62
- package/src/__tests__/pii-boot-gate.test.ts +0 -68
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/require-env.test.ts +0 -29
- package/src/__tests__/resolve-auth-mail.test.ts +0 -69
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
- package/src/__tests__/run-prod-app-spec.test.ts +0 -57
- package/src/__tests__/run-prod-app.integration.test.ts +0 -915
- package/src/__tests__/session-wiring.test.ts +0 -51
- package/src/__tests__/try-hono-first.test.ts +0 -63
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/apply-boot-seeds.ts +0 -19
- package/src/boot/boot-crypto.ts +0 -82
- package/src/boot/job-run-logger.ts +0 -51
- package/src/build-prod-bundle.ts +0 -692
- package/src/compose-features.ts +0 -164
- package/src/extra-routes-deps.ts +0 -47
- package/src/inject-schema.ts +0 -30
- package/src/pii-boot-gate.ts +0 -62
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -241
- package/src/run-prod-app-static-files.ts +0 -273
- package/src/run-prod-app.ts +0 -1158
- package/src/session-wiring.ts +0 -29
- package/src/try-hono-first.ts +0 -46
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type CachePolicy,
|
|
3
|
-
cachedResponse,
|
|
4
|
-
computeStrongEtag,
|
|
5
|
-
computeWeakEtag,
|
|
6
|
-
} from "@cosmicdrift/kumiko-framework/api";
|
|
7
|
-
import { ASSETS_DIR } from "./build-prod-bundle";
|
|
8
|
-
import { injectSchema } from "./inject-schema";
|
|
9
|
-
import type { HostDispatchFn } from "./run-prod-app";
|
|
10
|
-
import { tryHonoFirst } from "./try-hono-first";
|
|
11
|
-
|
|
12
|
-
// Static-asset + SPA-fallback serving for runProdApp's HTTP handler. Split
|
|
13
|
-
// out of run-prod-app.ts (#1005, Welle 2) — mechanical relocation, these
|
|
14
|
-
// functions are self-contained (no closure over runProdApp's local boot
|
|
15
|
-
// state), only params.
|
|
16
|
-
|
|
17
|
-
// Static-fallback: try the Hono app first, fall back to a file in
|
|
18
|
-
// staticDir if Hono returns 404. Keeps /api/* on the dispatcher and
|
|
19
|
-
// everything else (HTML, JS, CSS, images) on the disk.
|
|
20
|
-
//
|
|
21
|
-
// Cache-Header-Strategie:
|
|
22
|
-
// /assets/* → public, max-age=31536000, immutable
|
|
23
|
-
// (gehashte Filenames vom Build, sicher cachebar)
|
|
24
|
-
// /index.html → no-cache, must-revalidate
|
|
25
|
-
// (HTML-Shell, must reload on deploy)
|
|
26
|
-
// /manifest.json, /sw.js → no-cache
|
|
27
|
-
// (Update-Detection-Mechanismen, müssen frisch sein)
|
|
28
|
-
// alles andere → kein expliziter Header
|
|
29
|
-
// (Browser-Default, public/-Files wie favicon)
|
|
30
|
-
// File-reader für den static-fallback. Nutzt node:fs/promises statt
|
|
31
|
-
// Bun.file damit der Pfad in vitest+node integration-tests laufen kann
|
|
32
|
-
// (Bun.file ist Bun-only). Performance-cost ist marginal: die Disk-
|
|
33
|
-
// Files in einem prod-staticDir sind 1-200 KB, full-buffer-Read ist
|
|
34
|
-
// ein paar Mikrosekunden. Streaming via Bun.file wäre nur relevant ab
|
|
35
|
-
// ~1 MB.
|
|
36
|
-
export async function readStaticFile(
|
|
37
|
-
filePath: string,
|
|
38
|
-
): Promise<
|
|
39
|
-
{ readonly bytes: Uint8Array; readonly mime: string; readonly mtimeMs: number } | undefined
|
|
40
|
-
> {
|
|
41
|
-
try {
|
|
42
|
-
const { readFile, stat } = await import("node:fs/promises");
|
|
43
|
-
const [bytes, fileStat] = await Promise.all([readFile(filePath), stat(filePath)]);
|
|
44
|
-
return { bytes, mime: mimeTypeFor(filePath), mtimeMs: fileStat.mtimeMs };
|
|
45
|
-
} catch (err) {
|
|
46
|
-
if ((err as { code?: string }).code === "ENOENT") return undefined;
|
|
47
|
-
throw err;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function serveDiskFile(
|
|
52
|
-
req: Request,
|
|
53
|
-
pathname: string,
|
|
54
|
-
file: {
|
|
55
|
-
readonly bytes: Uint8Array;
|
|
56
|
-
readonly mime: string;
|
|
57
|
-
readonly mtimeMs: number;
|
|
58
|
-
},
|
|
59
|
-
): Response {
|
|
60
|
-
return cachedResponse(req, {
|
|
61
|
-
// @cast-boundary bun-types — Response BodyInit narrowing
|
|
62
|
-
body: file.bytes as unknown as BodyInit,
|
|
63
|
-
etag: computeWeakEtag(file.mtimeMs, file.bytes.byteLength),
|
|
64
|
-
cache: staticCachePolicy(pathname),
|
|
65
|
-
headers: { "content-type": file.mime },
|
|
66
|
-
lastModified: new Date(file.mtimeMs),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Minimal-Mime-Map — deckt die Files ab die kumiko-build und typische
|
|
71
|
-
// public/-Inhalte produzieren. Bun.file leitet das aus dem Suffix ab,
|
|
72
|
-
// im node-Pfad müssen wir es selbst tun. Default: octet-stream (Browser
|
|
73
|
-
// fragt bei unbekanntem MIME nach).
|
|
74
|
-
export function mimeTypeFor(filePath: string): string {
|
|
75
|
-
const ext = filePath.toLowerCase().split(".").pop() ?? "";
|
|
76
|
-
switch (ext) {
|
|
77
|
-
case "html":
|
|
78
|
-
return "text/html; charset=utf-8";
|
|
79
|
-
case "js":
|
|
80
|
-
case "mjs":
|
|
81
|
-
return "text/javascript; charset=utf-8";
|
|
82
|
-
case "css":
|
|
83
|
-
return "text/css; charset=utf-8";
|
|
84
|
-
case "json":
|
|
85
|
-
return "application/json; charset=utf-8";
|
|
86
|
-
case "svg":
|
|
87
|
-
return "image/svg+xml";
|
|
88
|
-
case "png":
|
|
89
|
-
return "image/png";
|
|
90
|
-
case "jpg":
|
|
91
|
-
case "jpeg":
|
|
92
|
-
return "image/jpeg";
|
|
93
|
-
case "ico":
|
|
94
|
-
return "image/x-icon";
|
|
95
|
-
case "txt":
|
|
96
|
-
return "text/plain; charset=utf-8";
|
|
97
|
-
case "xml":
|
|
98
|
-
return "application/xml; charset=utf-8";
|
|
99
|
-
case "webmanifest":
|
|
100
|
-
return "application/manifest+json";
|
|
101
|
-
default:
|
|
102
|
-
return "application/octet-stream";
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function buildStaticFallback(
|
|
107
|
-
apiHandler: (req: Request) => Response | Promise<Response>,
|
|
108
|
-
staticDir: string,
|
|
109
|
-
appSchemaJson: string,
|
|
110
|
-
hostDispatch?: HostDispatchFn,
|
|
111
|
-
): (req: Request) => Promise<Response> {
|
|
112
|
-
const indexHtml = `${staticDir}/index.html`;
|
|
113
|
-
|
|
114
|
-
// Helper: liest eine HTML-Datei von der Disk + (optional) injiziert
|
|
115
|
-
// das pre-serialized AppSchema vor dem client.js-Tag. Schema-Injection
|
|
116
|
-
// ist explicit-opt-in damit Public-Domain-Antworten die Admin-UI-
|
|
117
|
-
// Topologie nicht leaken. injectSchema ist idempotent, doppelte Calls
|
|
118
|
-
// produzieren keinen doppelten Tag.
|
|
119
|
-
async function readHtmlFile(
|
|
120
|
-
path: string,
|
|
121
|
-
injectSchemaInto: boolean,
|
|
122
|
-
): Promise<{ bytes: ArrayBuffer; mime: string; etag: string; mtimeMs: number } | null> {
|
|
123
|
-
const file = await readStaticFile(path);
|
|
124
|
-
if (!file) return null;
|
|
125
|
-
if (!injectSchemaInto) {
|
|
126
|
-
return {
|
|
127
|
-
bytes: file.bytes.buffer.slice(
|
|
128
|
-
file.bytes.byteOffset,
|
|
129
|
-
file.bytes.byteOffset + file.bytes.byteLength,
|
|
130
|
-
) as ArrayBuffer,
|
|
131
|
-
mime: file.mime,
|
|
132
|
-
etag: computeWeakEtag(file.mtimeMs, file.bytes.byteLength),
|
|
133
|
-
mtimeMs: file.mtimeMs,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
const text = new TextDecoder().decode(file.bytes);
|
|
137
|
-
const injected = injectSchema(text, appSchemaJson);
|
|
138
|
-
const bytes = new TextEncoder().encode(injected).buffer as ArrayBuffer;
|
|
139
|
-
return {
|
|
140
|
-
bytes,
|
|
141
|
-
mime: file.mime,
|
|
142
|
-
etag: computeStrongEtag(new Uint8Array(bytes)),
|
|
143
|
-
mtimeMs: file.mtimeMs,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function serveHtmlFile(
|
|
148
|
-
req: Request,
|
|
149
|
-
pathname: string,
|
|
150
|
-
html: { bytes: ArrayBuffer; mime: string; etag: string; mtimeMs: number },
|
|
151
|
-
extraHeaders?: Record<string, string>,
|
|
152
|
-
): Response {
|
|
153
|
-
return cachedResponse(req, {
|
|
154
|
-
body: html.bytes,
|
|
155
|
-
etag: html.etag,
|
|
156
|
-
cache: staticCachePolicy(pathname),
|
|
157
|
-
headers: { "content-type": html.mime, ...extraHeaders },
|
|
158
|
-
lastModified: new Date(html.mtimeMs),
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// hostDispatch konsultieren wenn gesetzt UND der Request auf den
|
|
163
|
-
// HTML-Fallback fällt (Root oder SPA-Route). Returnt entweder die
|
|
164
|
-
// resolved Response (redirect/404/html) oder null wenn der Default-
|
|
165
|
-
// Pfad weiterlaufen soll.
|
|
166
|
-
async function tryHostDispatch(req: Request): Promise<Response | null> {
|
|
167
|
-
if (!hostDispatch) return null;
|
|
168
|
-
const url = new URL(req.url);
|
|
169
|
-
const host = req.headers.get("host") ?? url.host;
|
|
170
|
-
const result = hostDispatch({ host, path: url.pathname, search: url.search });
|
|
171
|
-
if (result.kind === "not-found") {
|
|
172
|
-
return new Response("Not Found", { status: 404 });
|
|
173
|
-
}
|
|
174
|
-
if (result.kind === "redirect") {
|
|
175
|
-
return new Response(null, {
|
|
176
|
-
status: result.status ?? 302,
|
|
177
|
-
headers: { Location: result.to },
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
// result.kind === "html"
|
|
181
|
-
const filePath = `${staticDir}/${result.file}`;
|
|
182
|
-
const html = await readHtmlFile(filePath, result.injectSchema === true);
|
|
183
|
-
if (!html) {
|
|
184
|
-
// Author-Fehler: hostDispatch verweist auf nicht-existente Datei.
|
|
185
|
-
// Liefer 500 statt silent-404 damit der Bug schnell auffällt.
|
|
186
|
-
return new Response(`hostDispatch: file not found: ${result.file}`, { status: 500 });
|
|
187
|
-
}
|
|
188
|
-
// Per-Host-Body (hostDispatch wählt die Datei nach Host) → Vary: Host,
|
|
189
|
-
// sonst darf ein Shared-Cache Tenant-As Schema an Tenant B liefern.
|
|
190
|
-
const extraHeaders: Record<string, string> = { vary: "Host" };
|
|
191
|
-
if (result.csp) extraHeaders["content-security-policy"] = result.csp;
|
|
192
|
-
return serveHtmlFile(req, "/index.html", html, extraHeaders);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return async (req: Request): Promise<Response> => {
|
|
196
|
-
const url = new URL(req.url);
|
|
197
|
-
// /api/* and /health → always Hono (Dispatcher + Health-Probe).
|
|
198
|
-
if (url.pathname.startsWith("/api/") || url.pathname === "/health") {
|
|
199
|
-
return apiHandler(req);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Hono-First für andere Pfade: extraRoutes (z.B. /feed.xml,
|
|
203
|
-
// /sitemap.xml) UND r.httpRoute-Features (z.B. /legal/*) müssen vor
|
|
204
|
-
// dem Disk-Lookup greifen, sonst schluckt der SPA-Fallback unten
|
|
205
|
-
// unbekannte Pfade als index.html. Shared mit dev-server's
|
|
206
|
-
// createKumikoServer.handleFetch damit beide IDENTISCHE Semantik haben.
|
|
207
|
-
const honoTry = await tryHonoFirst({ fetch: apiHandler }, req);
|
|
208
|
-
if (honoTry.matched) {
|
|
209
|
-
return honoTry.response;
|
|
210
|
-
}
|
|
211
|
-
const honoRes = honoTry.response;
|
|
212
|
-
|
|
213
|
-
// Disk-/SPA-Fallback ist GET/HEAD-only. Ein non-GET ohne Hono-Match
|
|
214
|
-
// (z.B. POST auf einen falsch konfigurierten Webhook-Pfad) muss den
|
|
215
|
-
// Hono-404 durchreichen — 200 index.html würde dem Provider
|
|
216
|
-
// "delivered" signalisieren und Events gingen still verloren (#259).
|
|
217
|
-
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
218
|
-
return honoRes;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Disk-Datei (Asset oder konkrete File). Asset-Pfade laufen
|
|
222
|
-
// host-unabhängig — die Bundles in /assets/* werden vom client
|
|
223
|
-
// aktiv geladen, kein Server-side Routing nötig.
|
|
224
|
-
const isIndexRequest = url.pathname === "/" || url.pathname === "/index.html";
|
|
225
|
-
if (!isIndexRequest) {
|
|
226
|
-
const relPath = url.pathname.slice(1);
|
|
227
|
-
const filePath = `${staticDir}/${relPath}`;
|
|
228
|
-
const file = await readStaticFile(filePath);
|
|
229
|
-
if (file) {
|
|
230
|
-
return serveDiskFile(req, url.pathname, file);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Root oder SPA-Route — hier greift hostDispatch wenn gesetzt.
|
|
235
|
-
// Ohne hostDispatch: alter Single-App-Pfad (index.html mit Schema).
|
|
236
|
-
const dispatched = await tryHostDispatch(req);
|
|
237
|
-
if (dispatched) return dispatched;
|
|
238
|
-
|
|
239
|
-
// Default Single-App-Pfad: index.html, schema injected.
|
|
240
|
-
const index = await readHtmlFile(indexHtml, true);
|
|
241
|
-
if (index) {
|
|
242
|
-
return serveHtmlFile(req, "/index.html", index);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Kein Hono-Match, keine Disk-Datei, kein index.html → liefer den
|
|
246
|
-
// ursprünglichen 404 von Hono durch (statt einen neuen Roundtrip).
|
|
247
|
-
return honoRes;
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Map URL-Pfad → Cache-Policy. Hashed-Asset-Pfade (/assets/*) sind
|
|
252
|
-
// unveränderlich, der Rest bleibt revalidate/no-cache damit Updates ohne
|
|
253
|
-
// Hard-Reload greifen. Exported für Unit-Tests; Konsumenten gehen via
|
|
254
|
-
// runProdApp.
|
|
255
|
-
export function staticCachePolicy(pathname: string): CachePolicy {
|
|
256
|
-
if (pathname.startsWith(`/${ASSETS_DIR}/`)) {
|
|
257
|
-
return { kind: "immutable" };
|
|
258
|
-
}
|
|
259
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
260
|
-
return { kind: "revalidate" };
|
|
261
|
-
}
|
|
262
|
-
if (
|
|
263
|
-
pathname === "/manifest.json" ||
|
|
264
|
-
pathname === "/sw.js" ||
|
|
265
|
-
// ponytail: build-info.json ist statisch — kein /api/version-Endpoint
|
|
266
|
-
// nötig, der Disk-Fallback serviert sie. no-cache, sonst pollt der
|
|
267
|
-
// UpdateChecker eine veraltete id.
|
|
268
|
-
pathname === "/build-info.json"
|
|
269
|
-
) {
|
|
270
|
-
return { kind: "no-cache" };
|
|
271
|
-
}
|
|
272
|
-
return { kind: "none" };
|
|
273
|
-
}
|