first commit

This commit is contained in:
Tomas Dvorak
2026-04-10 12:04:09 +02:00
commit 3cb40adb23
203 changed files with 40226 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
#!/usr/bin/env node
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
const rootDir = process.cwd();
const manifestPath = path.resolve(rootDir, process.argv[2] || "apps/frontend/.vinxi/build/client/_build/.vite/manifest.json");
const assetsDir = path.resolve(path.dirname(path.dirname(manifestPath)), "assets");
const budget = {
maxTotalJsBytes: Number.parseInt(process.env.MAX_TOTAL_JS_BYTES || "320000", 10),
maxTotalCssBytes: Number.parseInt(process.env.MAX_TOTAL_CSS_BYTES || "50000", 10),
maxEntryJsBytes: Number.parseInt(process.env.MAX_ENTRY_JS_BYTES || "35000", 10),
maxRouteJsBytes: Number.parseInt(process.env.MAX_ROUTE_JS_BYTES || "45000", 10),
maxChunkJsBytes: Number.parseInt(process.env.MAX_CHUNK_JS_BYTES || "50000", 10)
};
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
async function readManifest(filePath) {
const raw = await readFile(filePath, "utf8");
return JSON.parse(raw);
}
function parseRouteEntries(manifest) {
return Object.entries(manifest)
.filter(([key]) => key.startsWith("src/routes/") && key.includes("?pick=default&pick=$css"))
.map(([key, value]) => ({
routeKey: key,
file: value?.file
}))
.filter(entry => typeof entry.file === "string" && entry.file.endsWith(".js"));
}
async function fileSizeForAsset(assetFile) {
const absolute = path.resolve(assetsDir, path.basename(assetFile));
const info = await stat(absolute);
return info.size;
}
async function main() {
const failures = [];
let manifest;
try {
manifest = await readManifest(manifestPath);
} catch (error) {
console.error(
`[error] failed to read frontend build manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
const values = Object.values(manifest).filter(Boolean);
const jsAssets = new Set();
const cssAssets = new Set();
for (const item of values) {
if (typeof item?.file === "string") {
if (item.file.endsWith(".js")) {
jsAssets.add(path.basename(item.file));
} else if (item.file.endsWith(".css")) {
cssAssets.add(path.basename(item.file));
}
}
if (Array.isArray(item?.css)) {
for (const cssFile of item.css) {
if (typeof cssFile === "string" && cssFile.endsWith(".css")) {
cssAssets.add(path.basename(cssFile));
}
}
}
}
let totalJsBytes = 0;
let totalCssBytes = 0;
let largestJsChunk = { name: "", size: 0 };
for (const jsFile of jsAssets) {
const size = await fileSizeForAsset(jsFile);
totalJsBytes += size;
if (size > largestJsChunk.size) {
largestJsChunk = { name: jsFile, size };
}
}
for (const cssFile of cssAssets) {
totalCssBytes += await fileSizeForAsset(cssFile);
}
const entryKey = "virtual:$vinxi/handler/client";
const entryFile = manifest[entryKey]?.file;
if (!entryFile) {
failures.push(`missing entry chunk in manifest: ${entryKey}`);
}
const entrySize = entryFile ? await fileSizeForAsset(entryFile) : 0;
const routeEntries = parseRouteEntries(manifest);
const routeSizes = [];
for (const route of routeEntries) {
routeSizes.push({
...route,
size: await fileSizeForAsset(route.file)
});
}
routeSizes.sort((left, right) => right.size - left.size);
const largestRoute = routeSizes[0] ?? { routeKey: "n/a", file: "n/a", size: 0 };
if (totalJsBytes > budget.maxTotalJsBytes) {
failures.push(`total JS size ${formatBytes(totalJsBytes)} exceeds budget ${formatBytes(budget.maxTotalJsBytes)}`);
}
if (totalCssBytes > budget.maxTotalCssBytes) {
failures.push(`total CSS size ${formatBytes(totalCssBytes)} exceeds budget ${formatBytes(budget.maxTotalCssBytes)}`);
}
if (entrySize > budget.maxEntryJsBytes) {
failures.push(`entry chunk ${path.basename(entryFile)} is ${formatBytes(entrySize)} (budget ${formatBytes(budget.maxEntryJsBytes)})`);
}
if (largestJsChunk.size > budget.maxChunkJsBytes) {
failures.push(
`largest JS chunk ${largestJsChunk.name} is ${formatBytes(largestJsChunk.size)} (budget ${formatBytes(budget.maxChunkJsBytes)})`,
);
}
if (largestRoute.size > budget.maxRouteJsBytes) {
failures.push(
`largest route chunk ${path.basename(largestRoute.file)} is ${formatBytes(largestRoute.size)} (budget ${formatBytes(budget.maxRouteJsBytes)})`,
);
}
console.log(`[info] frontend budget report (${manifestPath})`);
console.log(`[info] total JS: ${formatBytes(totalJsBytes)} (${jsAssets.size} files)`);
console.log(`[info] total CSS: ${formatBytes(totalCssBytes)} (${cssAssets.size} files)`);
console.log(`[info] entry JS: ${formatBytes(entrySize)} (${entryFile ? path.basename(entryFile) : "missing"})`);
console.log(`[info] largest JS chunk: ${formatBytes(largestJsChunk.size)} (${largestJsChunk.name || "none"})`);
console.log(
`[info] largest route chunk: ${formatBytes(largestRoute.size)} (${path.basename(largestRoute.file || "none")} / ${largestRoute.routeKey})`,
);
if (routeSizes.length) {
const topRoutes = routeSizes.slice(0, 5);
console.log("[info] top route chunks:");
for (const route of topRoutes) {
console.log(` - ${route.routeKey}: ${formatBytes(route.size)} (${path.basename(route.file)})`);
}
}
if (failures.length) {
for (const failure of failures) {
console.error(`[error] ${failure}`);
}
process.exit(1);
}
console.log("[ok] frontend bundle budgets passed.");
}
void main();