/** * @file no-hardcoded-routes.js * Enforces use of RouteConfig.ts instead of hardcoded route strings * * This rule prevents hardcoded route strings in: * - router.push() / router.replace() * - redirect() * - Link href * - a href * - revalidatePath() * - Any string literal matching route patterns */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce use of RouteConfig.ts instead of hardcoded route strings', category: 'Best Practices', recommended: true, }, fixable: 'code', schema: [], messages: { hardcodedRoute: 'Hardcoded route "{{route}}". Use routes from RouteConfig.ts instead: import { routes } from "@/lib/routing/RouteConfig"', hardcodedRedirect: 'Hardcoded redirect route "{{route}}". Use routes from RouteConfig.ts', hardcodedLink: 'Hardcoded link href "{{route}}". Use routes from RouteConfig.ts', hardcodedAnchor: 'Hardcoded anchor href "{{route}}". Use routes from RouteConfig.ts', hardcodedRevalidate: 'Hardcoded revalidatePath "{{route}}". Use routes from RouteConfig.ts', }, }, create(context) { // Route patterns that should be in RouteConfig const routePatterns = [ '^/auth/', '^/public/', '^/protected/', '^/sponsor/', '^/admin/', '^/league(s)?/', '^/race(s)?/', '^/team(s)?/', '^/driver(s)?/', '^/leaderboard(s)?/', '^/error/', '^/404$', '^/500$', '^/dashboard$', '^/onboarding$', '^/profile', '^/sponsor/signup$', ]; // Allowed exceptions (non-route paths) const allowedPaths = [ '^/api/', '^/legal/', '^/terms$', '^/privacy$', '^/support$', '^/$', // root is allowed ]; function isHardcodedRoute(value) { if (typeof value !== 'string') return false; // Check if it's an allowed path if (allowedPaths.some(pattern => new RegExp(pattern).test(value))) { return false; } // Check if it matches a route pattern return routePatterns.some(pattern => new RegExp(pattern).test(value)); } function getRouteConfigSuggestion(route) { // Map common routes to RouteConfig suggestions const routeMap = { '/auth/login': 'routes.auth.login', '/auth/signup': 'routes.auth.signup', '/auth/forgot-password': 'routes.auth.forgotPassword', '/auth/reset-password': 'routes.auth.resetPassword', '/': 'routes.public.home', '/leagues': 'routes.public.leagues', '/drivers': 'routes.public.drivers', '/teams': 'routes.public.teams', '/leaderboards': 'routes.public.leaderboards', '/races': 'routes.public.races', '/sponsor/signup': 'routes.public.sponsorSignup', '/dashboard': 'routes.protected.dashboard', '/onboarding': 'routes.protected.onboarding', '/profile': 'routes.protected.profile', '/profile/settings': 'routes.protected.profileSettings', '/profile/leagues': 'routes.protected.profileLeagues', '/profile/liveries': 'routes.protected.profileLiveries', '/profile/liveries/upload': 'routes.protected.profileLiveryUpload', '/profile/sponsorship-requests': 'routes.protected.profileSponsorshipRequests', '/sponsor': 'routes.sponsor.root', '/sponsor/dashboard': 'routes.sponsor.dashboard', '/sponsor/billing': 'routes.sponsor.billing', '/sponsor/campaigns': 'routes.sponsor.campaigns', '/sponsor/leagues': 'routes.sponsor.leagues', '/sponsor/settings': 'routes.sponsor.settings', '/admin': 'routes.admin.root', '/admin/users': 'routes.admin.users', '/404': 'routes.error.notFound', '/500': 'routes.error.serverError', }; // Check for parameterized routes const leagueMatch = route.match(/^\/leagues\/([^\/]+)$/); if (leagueMatch) { return `routes.league.detail('${leagueMatch[1]}')`; } const raceMatch = route.match(/^\/races\/([^\/]+)$/); if (raceMatch) { return `routes.race.detail('${raceMatch[1]}')`; } const teamMatch = route.match(/^\/teams\/([^\/]+)$/); if (teamMatch) { return `routes.team.detail('${teamMatch[1]}')`; } const driverMatch = route.match(/^\/drivers\/([^\/]+)$/); if (driverMatch) { return `routes.driver.detail('${driverMatch[1]}')`; } const sponsorLeagueMatch = route.match(/^\/sponsor\/leagues\/([^\/]+)$/); if (sponsorLeagueMatch) { return `routes.sponsor.leagueDetail('${sponsorLeagueMatch[1]}')`; } const leagueAdminMatch = route.match(/^\/leagues\/([^\/]+)\/schedule\/admin$/); if (leagueAdminMatch) { return `routes.league.scheduleAdmin('${leagueAdminMatch[1]}')`; } return routeMap[route] || null; } function reportHardcodedRoute(node, value, messageId, data = {}) { const suggestion = getRouteConfigSuggestion(value); const messageData = { route: value, ...data, }; context.report({ node, messageId, data: messageData, fix(fixer) { if (suggestion) { return fixer.replaceText(node, suggestion); } return null; }, }); } return { // Check router.push() and router.replace() CallExpression(node) { if (node.callee.type === 'MemberExpression' && (node.callee.property.name === 'push' || node.callee.property.name === 'replace')) { // Check if it's router.push/replace const calleeObj = node.callee.object; if (calleeObj.type === 'Identifier' && calleeObj.name === 'router') { const arg = node.arguments[0]; if (arg && arg.type === 'Literal' && isHardcodedRoute(arg.value)) { reportHardcodedRoute(arg, arg.value, 'hardcodedRoute'); } } } // Check redirect() if (node.callee.type === 'Identifier' && node.callee.name === 'redirect') { const arg = node.arguments[0]; if (arg && arg.type === 'Literal' && isHardcodedRoute(arg.value)) { reportHardcodedRoute(arg, arg.value, 'hardcodedRedirect'); } } // Check revalidatePath() if (node.callee.type === 'Identifier' && node.callee.name === 'revalidatePath') { const arg = node.arguments[0]; if (arg && arg.type === 'Literal' && isHardcodedRoute(arg.value)) { reportHardcodedRoute(arg, arg.value, 'hardcodedRevalidate'); } } }, // Check Link href JSXOpeningElement(node) { if (node.name.type === 'JSXIdentifier' && node.name.name === 'Link') { const hrefAttr = node.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.name === 'href' ); if (hrefAttr && hrefAttr.value && hrefAttr.value.type === 'Literal') { const value = hrefAttr.value.value; if (isHardcodedRoute(value)) { reportHardcodedRoute(hrefAttr.value, value, 'hardcodedLink'); } } } // Check a href if (node.name.type === 'JSXIdentifier' && node.name.name === 'a') { const hrefAttr = node.attributes.find(attr => attr.type === 'JSXAttribute' && attr.name.name === 'href' ); if (hrefAttr && hrefAttr.value && hrefAttr.value.type === 'Literal') { const value = hrefAttr.value.value; if (isHardcodedRoute(value)) { reportHardcodedRoute(hrefAttr.value, value, 'hardcodedAnchor'); } } } }, // Check template literals in href JSXAttribute(node) { if (node.name.name === 'href' && node.value && node.value.type === 'JSXExpressionContainer') { const expr = node.value.expression; if (expr.type === 'TemplateLiteral') { // Check if template literal contains hardcoded route for (const quasi of expr.quasis) { if (quasi.value && isHardcodedRoute(quasi.value.raw)) { reportHardcodedRoute(quasi, quasi.value.raw, 'hardcodedLink'); } } } } }, // Check string literals in general (catches other cases) Literal(node) { if (typeof node.value === 'string' && isHardcodedRoute(node.value)) { // Skip if it's already being handled by more specific checks const parent = node.parent; // Skip if it's in a comment or already reported if (parent.type === 'Property' && parent.key === node) { return; // Object property key } // Check if this is in a context we care about let isInRelevantContext = false; let messageId = 'hardcodedRoute'; // Walk up to find context let current = parent; while (current) { if (current.type === 'CallExpression') { if (current.callee.type === 'MemberExpression') { const prop = current.callee.property.name; if (prop === 'push' || prop === 'replace') { isInRelevantContext = true; break; } } if (current.callee.type === 'Identifier') { const name = current.callee.name; if (name === 'redirect' || name === 'revalidatePath') { isInRelevantContext = true; messageId = name === 'redirect' ? 'hardcodedRedirect' : 'hardcodedRevalidate'; break; } } } current = current.parent; } if (isInRelevantContext) { reportHardcodedRoute(node, node.value, messageId); } } }, }; }, };