mirror of
https://github.com/Dvorinka/Productier.git
synced 2026-06-04 12:33:01 +00:00
first commit
This commit is contained in:
@@ -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();
|
||||
Reference in New Issue
Block a user