#!/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();