clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / render / tools / lines.mjs
lines.mjs 3.1K
1// The seam's line budget, as a warning (issue #5 §5.5).2//3// The target is ~3,000 authored TypeScript lines, and it is a guideline, not4// a wall: it exists so that a seam quietly growing a second application5// inside itself is visible in CI rather than discovered later. So this counts,6// prints, and — past the budget — warns. It never fails a build.7//8// What counts as authored: code. Blank lines and comment-only lines are not9// counted, because a lane whose contract is its documentation should not be10// taxed for explaining itself; the totals are printed either way so the ratio11// stays visible. Tests are counted separately and are not part of the budget:12// a lane is not improved by being under-tested.1314import { readdirSync, readFileSync, statSync } from "node:fs";15import { dirname, join, relative } from "node:path";16import { fileURLToPath } from "node:url";1718const root = join(dirname(fileURLToPath(import.meta.url)), "..");19const BUDGET = 3000;2021function files(directory) {22 const found = [];23 for (const name of readdirSync(directory)) {24 if (name === "node_modules" || name === "dist") continue;25 const path = join(directory, name);26 if (statSync(path).isDirectory()) found.push(...files(path));27 else if (name.endsWith(".ts")) found.push(path);28 }29 return found;30}3132/** Code, comments and blanks, told apart well enough to be honest. */33function measure(source) {34 let code = 0;35 let comment = 0;36 let blank = 0;37 let inBlock = false;38 for (const raw of source.split("\n")) {39 const line = raw.trim();40 if (inBlock) {41 comment++;42 if (line.includes("*/")) inBlock = false;43 continue;44 }45 if (!line) blank++;46 else if (line.startsWith("//")) comment++;47 else if (line.startsWith("/*")) {48 comment++;49 inBlock = !line.includes("*/");50 } else code++;51 }52 return { code, comment, blank };53}5455const rows = [];56let lane = { code: 0, comment: 0, blank: 0 };57let tests = { code: 0, comment: 0, blank: 0 };58for (const path of files(root).sort()) {59 const counts = measure(readFileSync(path, "utf8"));60 const where = relative(root, path);61 rows.push({ where, ...counts });62 const into = where.startsWith("test/") ? tests : lane;63 into.code += counts.code;64 into.comment += counts.comment;65 into.blank += counts.blank;66}6768const width = Math.max(...rows.map((row) => row.where.length));69for (const row of rows) {70 process.stdout.write(71 `${row.where.padEnd(width)} ${String(row.code).padStart(5)} code ` +72 `${String(row.comment).padStart(5)} prose\n`);73}74process.stdout.write(75 `\nthe seam: ${lane.code} authored lines of code, ${lane.comment} of prose ` +76 `(budget ${BUDGET})\nits tests: ${tests.code} lines of code, ${tests.comment} of prose\n`);7778if (lane.code > BUDGET) {79 process.stdout.write(80 `\n::warning::the seam is ${lane.code - BUDGET} lines past its ~${BUDGET}-line ` +81 `guideline (issue #5 §5.5). This is a warning, not a wall: either the seam ` +82 `has grown a second application inside it, or the budget wants revising in ` +83 `the issue. Both are decisions, and neither is silent.\n`);84}