Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-route-inventory.mjs
T

134 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const FILENAME = fileURLToPath(import.meta.url);
const REPO_ROOT = path.resolve(FILENAME, "..", "..");
const ROUTES_DIR = path.join(REPO_ROOT, "apps", "web", "app", "routes");
export const ROUTE_INVENTORY_BUDGETS = Object.freeze({
admin: 167,
tenant: 78,
public_help_docs_internal: 58,
optional: 208,
total: 474,
});
const OPTIONAL_ROUTE_PATTERNS = Object.freeze([
/^help(\/|$)/,
/^mcp(\/|$)/,
/^internal\/api\/__workflows(\/|$)/,
/^admin\/?__workflows(\/|$)/,
/^admin\/ai(\/|$)/,
/^admin\/chat(\/|$)/,
/^admin\/crm(\.|\/|$)/,
/^admin\/email-marketing(\.|\/|$)/,
/^admin\/help-desk(\.|\/|$)/,
/^admin\/knowledge-base(\.|\/|$)/,
/^admin\/portals(\.|\/|$)/,
/^admin\/prompts(\.|\/|$)/,
/^app\.\$tenant\/__widgets(\/|$)/,
/^app\.\$tenant\/__workflows(\/|$)/,
/^app\.\$tenant\/chat(\/|$)/,
/^app\.\$tenant\/crm(\.|\/|$)/,
/^app\.\$tenant\/email-marketing(\.|\/|$)/,
/^app\.\$tenant\/internal\/ai(\/|$)/,
/^app\.\$tenant\/__portals(\/|$)/,
/^app\.\$tenant\/sites(\/|$)/,
]);
function isRouteFile(fileName) {
return /\.(ts|tsx)$/.test(fileName) && !/\.test\.(ts|tsx)$/.test(fileName) && !/\.spec\.(ts|tsx)$/.test(fileName) && !/\.server\.(ts|tsx)$/.test(fileName);
}
async function walkRoutes(dir = ROUTES_DIR) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkRoutes(fullPath)));
} else if (entry.isFile() && isRouteFile(entry.name)) {
files.push(path.relative(ROUTES_DIR, fullPath).split(path.sep).join("/"));
}
}
return files.sort((left, right) => left.localeCompare(right));
}
export function classifyRouteSurface(relativePath) {
if (OPTIONAL_ROUTE_PATTERNS.some((pattern) => pattern.test(relativePath))) {
return "optional";
}
if (relativePath.startsWith("admin") || relativePath.startsWith("admin/")) {
return "admin";
}
if (relativePath.startsWith("app.$tenant")) {
return "tenant";
}
return "public_help_docs_internal";
}
export function classifyRouteModule(relativePath) {
const surface = classifyRouteSurface(relativePath);
if (surface === "optional") {
return "optional";
}
if (surface === "admin" || surface === "tenant") {
return "default-enabled";
}
return "core";
}
export async function scanRouteInventory() {
const files = await walkRoutes();
const surfaces = { admin: 0, tenant: 0, public_help_docs_internal: 0, optional: 0, total: files.length };
const classifications = { core: 0, "default-enabled": 0, optional: 0 };
for (const file of files) {
surfaces[classifyRouteSurface(file)] += 1;
classifications[classifyRouteModule(file)] += 1;
}
return { files, surfaces, classifications };
}
export function evaluateRouteInventory(inventory, budgets = ROUTE_INVENTORY_BUDGETS) {
return Object.entries(budgets).map(([surface, budget]) => {
const count = inventory.surfaces[surface] ?? 0;
return { surface, count, budget, pass: count <= budget };
});
}
export async function runRouteInventoryGate({ logger = console } = {}) {
const inventory = await scanRouteInventory();
const results = evaluateRouteInventory(inventory);
const failures = results.filter((result) => !result.pass);
for (const result of results) {
const writer = result.pass ? logger.log : logger.error;
writer(`${result.pass ? "PASS" : "FAIL"} route_inventory surface=${result.surface} count=${result.count} budget=${result.budget}`);
}
logger.log(
`Route classifications: core=${inventory.classifications.core} default-enabled=${inventory.classifications["default-enabled"]} optional=${inventory.classifications.optional}`
);
logger.log(`Summary: ${results.length - failures.length} passed, ${failures.length} failed`);
return { inventory, results, failures };
}
async function main() {
const outcome = await runRouteInventoryGate();
if (outcome.failures.length > 0) {
process.exit(1);
}
}
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === FILENAME;
if (invokedDirectly) {
main().catch((error) => {
// eslint-disable-next-line no-console -- CLI gate must surface fatal errors on stderr.
console.error(error);
process.exit(1);
});
}