142 lines
4.5 KiB
JavaScript
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);
|
|
}
|
|
}
|