134 lines
4.4 KiB
JavaScript
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);
|
|
});
|
|
}
|