Files
alla-tms/scripts/check-routes.js
2026-07-16 09:53:14 +07:00

142 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ROOT = process.cwd();
const dashboardRoot = path.join(ROOT, 'src', 'app', 'dashboard');
const scanRoots = [
path.join(ROOT, 'src', 'app'),
path.join(ROOT, 'src', 'features'),
path.join(ROOT, 'src', 'config')
];
const legacyNavRoutes = [
'/dashboard/billing',
'/dashboard/chat',
'/dashboard/elements/icons',
'/dashboard/exclusive',
'/dashboard/forms',
'/dashboard/kanban',
'/dashboard/product',
'/dashboard/react-query',
'/dashboard/workspaces'
];
function walk(dir, predicate = () => true, results = []) {
if (!fs.existsSync(dir)) {
return results;
}
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(fullPath, predicate, results);
continue;
}
if (predicate(fullPath)) {
results.push(fullPath);
}
}
return results;
}
function toRoutePattern(filePath) {
const relative = path.relative(dashboardRoot, filePath).replace(/\\/g, '/');
const withoutPage = relative.replace(/\/page\.tsx$/, '').replace(/^page\.tsx$/, '');
const routePath = withoutPage ? `/dashboard/${withoutPage}` : '/dashboard';
return routePath.split('/').filter(Boolean);
}
function matchesRoute(routePattern, routePath) {
const targetSegments = routePath.split('/').filter(Boolean);
if (routePattern.length !== targetSegments.length) {
return false;
}
return routePattern.every((segment, index) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
return targetSegments[index].length > 0;
}
return segment === targetSegments[index];
});
}
function collectRoutePatterns() {
return walk(dashboardRoot, (filePath) => filePath.endsWith(`${path.sep}page.tsx`)).map(
toRoutePattern
);
}
function collectDashboardReferences() {
const routeRegex = /\/dashboard(?:\/[A-Za-z0-9-_[\]/]+)?/g;
const references = new Map();
for (const root of scanRoots) {
const files = walk(root, (filePath) => /\.(ts|tsx|js|jsx|md)$/.test(filePath));
for (const filePath of files) {
const content = fs.readFileSync(filePath, 'utf8');
const matches = content.match(routeRegex) ?? [];
for (const match of matches) {
const normalized = match.replace(/\/+$/, '');
if (!references.has(normalized)) {
references.set(normalized, []);
}
references.get(normalized).push(path.relative(ROOT, filePath).replace(/\\/g, '/'));
}
}
}
return references;
}
function main() {
const routePatterns = collectRoutePatterns();
const references = collectDashboardReferences();
const missingRoutes = [];
for (const [routePath, files] of references.entries()) {
const exists = routePatterns.some((pattern) => matchesRoute(pattern, routePath));
if (!exists) {
missingRoutes.push({ routePath, files });
}
}
const navConfigPath = path.join(ROOT, 'src', 'config', 'nav-config.ts');
const navConfig = fs.existsSync(navConfigPath) ? fs.readFileSync(navConfigPath, 'utf8') : '';
const leakedLegacyRoutes = legacyNavRoutes.filter((routePath) => navConfig.includes(routePath));
if (missingRoutes.length === 0 && leakedLegacyRoutes.length === 0) {
console.log('check-routes: passed');
return;
}
if (missingRoutes.length > 0) {
console.error('check-routes: missing dashboard route targets detected');
for (const { routePath, files } of missingRoutes) {
console.error(`- ${routePath}`);
console.error(` referenced from: ${files.join(', ')}`);
}
}
if (leakedLegacyRoutes.length > 0) {
console.error('check-routes: legacy/demo routes are still present in nav-config');
for (const routePath of leakedLegacyRoutes) {
console.error(`- ${routePath}`);
}
}
process.exitCode = 1;
}
main();