admin/scripts/check-bundle-budget.mjs
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

91 lines
3.6 KiB
JavaScript

import { gzipSync } from 'node:zlib'
import { readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
const chunksDirectory = join(process.cwd(), '.next/static/chunks')
const routeStatsPath = join(process.cwd(), '.next/diagnostics/route-bundle-stats.json')
const files = readdirSync(chunksDirectory).filter((file) => file.endsWith('.js'))
// User-facing budgets are based on what a route downloads on first load. The
// sum of every generated chunk is informational only: adding a lazy route or
// splitting a feature creates more files without increasing a user's payload.
const MAX_ROUTE_FIRST_LOAD_GZIP_KIB = 720
const MAX_SHARED_FIRST_LOAD_GZIP_KIB = 330
const MAX_SINGLE_RAW_KIB = 900
const chunkMetrics = new Map()
let totalGzipBytes = 0
let largest = { bytes: 0, file: '' }
const getChunkMetrics = (chunkPath) => {
const cached = chunkMetrics.get(chunkPath)
if (cached) return cached
const source = readFileSync(join(process.cwd(), chunkPath))
const metrics = { gzipBytes: gzipSync(source).byteLength, rawBytes: source.byteLength }
chunkMetrics.set(chunkPath, metrics)
return metrics
}
for (const file of files) {
const source = readFileSync(join(chunksDirectory, file))
totalGzipBytes += gzipSync(source).byteLength
if (source.byteLength > largest.bytes) largest = { bytes: source.byteLength, file }
}
const routeStats = JSON.parse(readFileSync(routeStatsPath, 'utf8'))
if (!Array.isArray(routeStats) || routeStats.length === 0) {
throw new Error('Next.js route bundle statistics are missing or empty')
}
const routeUsage = new Map()
let largestRoute = { gzipBytes: 0, route: '' }
for (const entry of routeStats) {
if (typeof entry.route !== 'string' || !Array.isArray(entry.firstLoadChunkPaths)) {
throw new Error('Next.js route bundle statistics have an unexpected shape')
}
const uniqueChunkPaths = new Set(entry.firstLoadChunkPaths)
let routeGzipBytes = 0
for (const chunkPath of uniqueChunkPaths) {
routeGzipBytes += getChunkMetrics(chunkPath).gzipBytes
routeUsage.set(chunkPath, (routeUsage.get(chunkPath) ?? 0) + 1)
}
if (routeGzipBytes > largestRoute.gzipBytes) largestRoute = { gzipBytes: routeGzipBytes, route: entry.route }
}
const sharedGzipBytes = [...routeUsage.entries()]
.filter(([, routeCount]) => routeCount === routeStats.length)
.reduce((total, [chunkPath]) => total + getChunkMetrics(chunkPath).gzipBytes, 0)
const totalGzipKiB = Math.ceil(totalGzipBytes / 1024)
const largestRawKiB = Math.ceil(largest.bytes / 1024)
const largestRouteGzipKiB = Math.ceil(largestRoute.gzipBytes / 1024)
const sharedGzipKiB = Math.ceil(sharedGzipBytes / 1024)
const failures = []
if (largestRouteGzipKiB > MAX_ROUTE_FIRST_LOAD_GZIP_KIB) {
failures.push(`route first-load ${largestRoute.route} ${largestRouteGzipKiB} KiB gzip > ${MAX_ROUTE_FIRST_LOAD_GZIP_KIB} KiB`)
}
if (sharedGzipKiB > MAX_SHARED_FIRST_LOAD_GZIP_KIB) {
failures.push(`shared first-load ${sharedGzipKiB} KiB gzip > ${MAX_SHARED_FIRST_LOAD_GZIP_KIB} KiB`)
}
if (largestRawKiB > MAX_SINGLE_RAW_KIB) failures.push(`largest chunk ${largest.file} ${largestRawKiB} KiB > ${MAX_SINGLE_RAW_KIB} KiB`)
console.log(
`Bundle budget: route ${largestRoute.route} ${largestRouteGzipKiB}/${MAX_ROUTE_FIRST_LOAD_GZIP_KIB} KiB gzip; shared ${sharedGzipKiB}/${MAX_SHARED_FIRST_LOAD_GZIP_KIB} KiB gzip; largest ${largestRawKiB}/${MAX_SINGLE_RAW_KIB} KiB raw (${largest.file})`
)
console.log(`All generated JavaScript: ${totalGzipKiB} KiB gzip (informational, includes lazy chunks for every route)`)
if (failures.length) {
console.error(failures.join('\n'))
process.exit(1)
}