// Bundle-Budget fuer `motion` im DOM (Spec T5 §17.6: hoechstens 30 kB gzip). // Laeuft nach `next build` wie check-3d-budget.mjs; Exit 1, wenn die motion-Chunks // zusammen das Budget ueberschreiten oder gar kein motion-Chunk gefunden wird. import { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { gzipSync } from "node:zlib"; // Spec §17.6: hoechstens 30 kB gzip (1 kB = 1000 Byte) fuer motion insgesamt: // den Teil in der App-Huelle und die asynchron geladenen Features. export const BUDGET_BYTES = 30_000; // String-Literale, die motion auch minifiziert behaelt und die kein App-Code schreibt: // der Stil-Schluessel der Render-Basis, das Attribut von AnimatePresence (popLayout) // und die Gesten-Props der Features. export const MOTION_MARKERS = ["transformPerspective", "data-motion-pop-id", "whileHover"]; const THREE_MARKERS = ["WebGLRenderer", "__THREE__"]; export function isMotionChunk(source) { if (THREE_MARKERS.every((marker) => source.includes(marker))) return false; return MOTION_MARKERS.some((marker) => source.includes(marker)); } /** * Misst alle motion-Chunks unter chunksDir (gzip level 9) und zaehlt sie zusammen. * @returns {{ ok: boolean, files: { file: string, gzipBytes: number }[], gzipBytes: number, budget: number, message: string }} */ export function checkMotionBudget(chunksDir, budget = BUDGET_BYTES) { const files = []; for (const name of readdirSync(chunksDir)) { if (!name.endsWith(".js")) continue; const source = readFileSync(path.join(chunksDir, name)); if (!isMotionChunk(source.toString("latin1"))) continue; files.push({ file: name, gzipBytes: gzipSync(source, { level: 9 }).length }); } const gzipBytes = files.reduce((sum, entry) => sum + entry.gzipBytes, 0); if (files.length === 0) { return { ok: false, files, gzipBytes, budget, message: `Kein motion-Chunk in ${chunksDir} gefunden (Marker: ${MOTION_MARKERS.join(", ")}). Check kaputt oder Bundle umgebaut?`, }; } const ok = gzipBytes <= budget; const list = files.map((entry) => `${entry.file} ${entry.gzipBytes}`).join(", "); return { ok, files, gzipBytes, budget, message: `motion: ${gzipBytes} Byte gzip in ${files.length} Chunks (${list}), Budget ${budget} Byte${ok ? "" : " UEBERSCHRITTEN"}`, }; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const chunksDir = path.resolve(process.cwd(), ".next/static/chunks"); let result; try { result = checkMotionBudget(chunksDir); } catch (error) { console.error(`check-motion-budget: ${chunksDir} nicht lesbar (zuerst next build?): ${error.message}`); process.exit(1); } (result.ok ? console.log : console.error)(result.message); process.exit(result.ok ? 0 : 1); }