Files
watermaps/apps/web/scripts/check-chunks.mjs
T
2026-07-24 11:29:24 +02:00

142 lines
4.5 KiB
JavaScript

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);
}
}