- Update AGENTS.md with 400-line hard ceiling and sub-feature slicing rules - Update scripts/lint_arch.ts to enforce 400 lines max with anti-formatting heuristics - Subdivide src/shared/ui/ into layout, navbar, and admin layout fragments - Subdivide src/features/auth/ into modular login, register, and recovery routes/fragments - Subdivide src/features/admin/ into users, user details, apps, and audit fragments - Maintain backwards compatibility via fragment index re-exports
94 lines
2.9 KiB
TypeScript
94 lines
2.9 KiB
TypeScript
import { walk } from "jsr:@std/fs";
|
|
|
|
const TARGET_DIR = "./src";
|
|
const MAX_FILE_LINES = 400;
|
|
|
|
// Auditable allowlist for exceptional files that legitimately exceed MAX_FILE_LINES.
|
|
// Every entry must be explicitly documented and approved.
|
|
const ALLOWLISTED_LARGE_FILES = new Set<string>([
|
|
// Currently empty: all modules in src/ must strictly conform to <= 400 lines.
|
|
]);
|
|
|
|
let hasErrors = false;
|
|
|
|
async function checkFile(path: string) {
|
|
const content = await Deno.readTextFile(path);
|
|
const lines = content.split("\n");
|
|
|
|
// 1. Enforce strict 400-line ceiling
|
|
if (lines.length > MAX_FILE_LINES && !ALLOWLISTED_LARGE_FILES.has(path)) {
|
|
console.error(
|
|
`[Arch Lint] ❌ File size ceiling exceeded in ${path}: ${lines.length} lines (Hard Ceiling: ${MAX_FILE_LINES} lines).`,
|
|
);
|
|
console.error(
|
|
` -> Subdivide this module into focused, SRP-aligned component files within the feature directory (e.g. login_fragments.tsx, register_fragments.tsx).`,
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
lines.forEach((line, index) => {
|
|
// 2. Block anti-formatting minification hacks (lines over 300 chars without SVG/CSS/raw string reasons)
|
|
if (
|
|
line.length > 300 &&
|
|
!line.includes("<svg") &&
|
|
!line.includes("data:image") &&
|
|
!line.includes("style=") &&
|
|
!line.includes("`") &&
|
|
!line.includes("/*")
|
|
) {
|
|
console.error(
|
|
`[Arch Lint] ❌ Anti-formatting detected (abnormally long line: ${line.length} chars) in ${path}:${
|
|
index + 1
|
|
}`,
|
|
);
|
|
console.error(
|
|
` -> Format code with standard 'deno fmt'. Do not minify to bypass line limits.`,
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
// 3. Block banned DOM APIs in src/
|
|
if (
|
|
line.includes("document.getElementById") ||
|
|
line.includes("document.querySelector") ||
|
|
line.includes("document.createElement")
|
|
) {
|
|
console.error(
|
|
`[Arch Lint] ❌ Banned DOM API used in ${path}:${index + 1}`,
|
|
);
|
|
console.error(` ${line.trim()}`);
|
|
console.error(
|
|
` -> Use Datastar reactive attributes or SSE morphs instead.`,
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
|
|
// 4. Block unescaped HTML in raw strings (basic heuristic for dangerouslySetInnerHTML)
|
|
if (
|
|
line.includes("dangerouslySetInnerHTML") &&
|
|
!path.includes("error_fragments.tsx")
|
|
) {
|
|
console.error(
|
|
`[Arch Lint] ❌ dangerouslySetInnerHTML used in ${path}:${index + 1}`,
|
|
);
|
|
console.error(` ${line.trim()}`);
|
|
console.error(
|
|
` -> Native JSX HTML escaping should be used unless in explicit core fragments.`,
|
|
);
|
|
hasErrors = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
for await (const entry of walk(TARGET_DIR, { exts: [".ts", ".tsx"] })) {
|
|
if (entry.isFile) {
|
|
await checkFile(entry.path);
|
|
}
|
|
}
|
|
|
|
if (hasErrors) {
|
|
Deno.exit(1);
|
|
} else {
|
|
console.log("✅ Architecture lint passed (All files <= 400 lines & clean).");
|
|
}
|