commit
This commit is contained in:
141
scripts/check-routes.js
Normal file
141
scripts/check-routes.js
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user