import { existsSync, readFileSync, statSync } from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; const webRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const distRoot = path.join(webRoot, "dist"); const manifestPath = path.join(distRoot, ".vite", "manifest.json"); assert(existsSync(manifestPath), "Vite-Manifest fehlt. Zuerst den Produktions-Build ausführen."); const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); const entry = manifest["index.html"]; assert(entry?.isEntry, "Der Web-Einstieg fehlt im Vite-Manifest."); const expectedDynamicEntries = [ "src/components/MapView.tsx", "src/components/AnchorWatchPanel.tsx", "src/components/CourseAssistantPanel.tsx", "src/components/VoyageNavigationTools.tsx", "src/routeWeatherReport.ts" ]; for (const key of expectedDynamicEntries) { const chunk = manifest[key]; assert(chunk?.isDynamicEntry, `${key} ist kein dynamischer Einstieg mehr.`); assertFile(chunk.file); } const mapEngineEntry = Object.entries(manifest).find(([, chunk]) => chunk.name === "map-engine"); assert(mapEngineEntry, "Der isolierte MapLibre-Chunk fehlt."); const [mapEngineKey, mapEngine] = mapEngineEntry; assertFile(mapEngine.file); assert( !entry.imports?.includes(mapEngineKey), "Der MapLibre-Chunk wird wieder statisch vom App-Einstieg geladen." ); assert( manifest["src/components/MapView.tsx"].imports?.includes(mapEngineKey), "MapView verweist nicht auf den isolierten MapLibre-Chunk." ); const mapViewChunk = manifest["src/components/MapView.tsx"]; assert(mapViewChunk.css?.length, "Das MapLibre-Stylesheet ist nicht mehr an MapView gekoppelt."); assert( mapViewChunk.css.every((file) => !entry.css?.includes(file)), "Das MapLibre-Stylesheet wird wieder vom App-Einstieg geladen." ); const initialChunkKeys = collectStaticImports("index.html"); const initialBytes = [...initialChunkKeys].reduce( (total, key) => total + fileSize(manifest[key].file), 0 ); assert( initialBytes <= 350_000, `Initiales JavaScript ist mit ${formatKb(initialBytes)} größer als das Budget von 350 kB.` ); const mapEngineBytes = fileSize(mapEngine.file); assert( mapEngineBytes <= 1_100_000, `Der MapLibre-Chunk ist mit ${formatKb(mapEngineBytes)} unerwartet gewachsen.` ); for (const chunk of Object.values(manifest)) { if (chunk.file?.endsWith(".js") && chunk.file !== mapEngine.file) { const bytes = fileSize(chunk.file); assert( bytes <= 350_000, `${chunk.file} ist mit ${formatKb(bytes)} zu groß und sollte weiter aufgeteilt werden.` ); } } const initialCssBytes = (entry.css ?? []).reduce( (total, file) => total + fileSize(file), 0 ); assert( initialCssBytes <= 40_000, `Initiales CSS ist mit ${formatKb(initialCssBytes)} größer als das Budget von 40 kB.` ); const html = readFileSync(path.join(distRoot, "index.html"), "utf8"); assert( !html.includes(path.basename(mapEngine.file)), "index.html lädt den dynamischen MapLibre-Chunk per modulepreload." ); const serviceWorkerPath = path.join(distRoot, "sw.js"); assert(existsSync(serviceWorkerPath), "Der PWA-Service-Worker fehlt."); const serviceWorker = readFileSync(serviceWorkerPath, "utf8"); const offlineFiles = [ mapEngine.file, ...mapViewChunk.css, ...expectedDynamicEntries.map((key) => manifest[key].file) ]; for (const file of offlineFiles) { assert( serviceWorker.includes(file), `${file} fehlt im PWA-Precache und wäre offline nicht zuverlässig verfügbar.` ); } console.log( `Chunk-Prüfung erfolgreich: initial ${formatKb(initialBytes)} JS + ${formatKb(initialCssBytes)} CSS, Karte ${formatKb(mapEngineBytes)}, ${expectedDynamicEntries.length} dynamische Funktionsmodule.` ); function collectStaticImports(rootKey) { const collected = new Set(); const visit = (key) => { if (collected.has(key)) { return; } const chunk = manifest[key]; assert(chunk, `Manifest-Verweis ${key} fehlt.`); collected.add(key); for (const dependency of chunk.imports ?? []) { visit(dependency); } }; visit(rootKey); return collected; } function assertFile(relativePath) { assert( typeof relativePath === "string" && existsSync(path.join(distRoot, relativePath)), `Chunk-Datei ${relativePath ?? "(unbekannt)"} fehlt.` ); } function fileSize(relativePath) { return statSync(path.join(distRoot, relativePath)).size; } function formatKb(bytes) { return `${(bytes / 1_000).toFixed(1)} kB`; } function assert(condition, message) { if (!condition) { throw new Error(message); } }