123const CEILING = 13_400;456const TS_AREAS: [string, string[], number][] = [7 ["bucket", ["src/bucket/"], 1170],8 ["mirror", ["src/mirror/"], 1100],9 ["describe", ["src/describe/"], 530],10 ["pages", ["src/pages/"], 2600],11 ["releases", ["src/releases/"], 900],12 ["tarball", ["src/tarball/"], 300],13 ["issues", ["src/issues/"], 1700],14 ["admin", ["src/admin/"], 2800],15 ["import-kepr2", ["scripts/import-kepr2.ts"], 300],16 ["server", ["src/server/", "src/main.ts", "scripts/budget.ts"], 2000],17];18const ASSET_AREAS: [string, string, number][] = [19 ["public/js", "public/js/**/*.js", 1400],20 ["public/css", "public/css/**/*.css", 3700],21];2223async function lineCount(path: string): Promise<number> {24 const text = await Bun.file(path).text();25 return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);26}2728async function countFiles(pattern: string): Promise<Map<string, number>> {29 const counts = new Map<string, number>();30 for await (const path of new Bun.Glob(pattern).scan(".")) counts.set(path, await lineCount(path));31 return counts;32}3334const tsFiles = await countFiles("{src,scripts}/**/*.ts");35const totals = new Map(TS_AREAS.map(([name]) => [name, 0]));36const unassigned: string[] = [];37for (const [path, lines] of tsFiles) {38 if (path.endsWith(".test.ts")) continue;39 const area = TS_AREAS.find(([, prefixes]) => prefixes.some((prefix) => path.startsWith(prefix)));40 if (!area) unassigned.push(path);41 else totals.set(area[0], (totals.get(area[0]) ?? 0) + lines);42}4344const rows: [string, number, number][] = TS_AREAS.map(([name, , budget]) => [45 name,46 totals.get(name) ?? 0,47 budget,48]);49const total = rows.reduce((sum, [, lines]) => sum + lines, 0);50for (const [name, pattern, budget] of ASSET_AREAS) {51 const files = await countFiles(pattern);52 rows.push([name, [...files.values()].reduce((a, b) => a + b, 0), budget]);53}5455let failed = false;56for (const [name, lines, budget] of rows) {57 const over = lines > budget;58 failed ||= over;59 console.log(`${name.padEnd(14)} ${String(lines).padStart(6)} / ${budget}${over ? " OVER" : ""}`);60}61console.log(`${"typescript".padEnd(14)} ${String(total).padStart(6)} / ${CEILING}`);62if (total > CEILING) failed = true;63for (const path of unassigned) console.error(`no budget area for ${path}`);64if (failed || unassigned.length > 0) process.exit(1);