import { Idx, Path } from '@wollybeard/kit' import { type Route, type RouteFile, routeIsFromIndexFile, routeToPathExpression } from './route.js' export type DiagnosticSeverity = `error` | `warning` | `info` export type Diagnostic = DiagnosticIndexConflict | DiagnosticNumberedPrefixConflict | DiagnosticNumberedPrefixOnIndex export interface DiagnosticIndexConflict { type: `index-conflict` severity: DiagnosticSeverity message: string literal: { file: RouteFile } index: { file: RouteFile } } export interface DiagnosticNumberedPrefixConflict { type: `numbered-prefix-conflict` severity: DiagnosticSeverity message: string kept: { file: RouteFile order: number } dropped: { file: RouteFile order: number } } export interface DiagnosticNumberedPrefixOnIndex { type: `numbered-prefix-on-index` severity: DiagnosticSeverity message: string file: RouteFile order: number } export interface LintResult { diagnostics: Diagnostic[] routes: Route[] } export const lint = (routes: Route[]): LintResult => { const diagnostics: Diagnostic[] = [] const seen = Idx.create({ key: routeToPathExpression }) // ━ Check for numbered prefix on index files for (const route of routes) { if (routeIsFromIndexFile(route) && route.logical.order !== undefined) { const diagnostic: DiagnosticNumberedPrefixOnIndex = { type: `numbered-prefix-on-index`, severity: `warning`, message: `Numbered prefix on index file has no effect. The file:\n ${ Path.format(route.file.path.relative) }\n\nhas a numbered prefix (${route.logical.order}_) which doesn't affect ordering since index files represent their parent directory.`, file: route.file, order: route.logical.order, } diagnostics.push(diagnostic) } } // ━ Check for conflicts for (const route of routes) { // Detect const seenRoute = seen.get(route) if (seenRoute) { // Check if it's a numbered prefix conflict if (seenRoute.logical.order !== undefined && route.logical.order !== undefined) { // Handle numbered prefix conflict - keep the one with higher order const [kept, dropped] = seenRoute.logical.order > route.logical.order ? [seenRoute, route] : [route, seenRoute] if (dropped === seenRoute) { seen.set(kept) } const orderMessage = kept.logical.order === dropped.logical.order ? `Both files have the same order number (${kept.logical.order}). The file processed later is being kept based on processing order.` : `The file with lower order number (${dropped.logical.order}) is being dropped in favor of the one with higher order (${kept.logical.order}).` const diagnostic: DiagnosticNumberedPrefixConflict = { type: `numbered-prefix-conflict`, severity: `error`, // dprint-ignore message: `Your files represent conflicting routes due to numbered prefixes. This file:\n ${Path.format(kept.file.path.relative)}\n\nconflicts with this file:\n\n ${Path.format(dropped.file.path.relative)}.\n\n${orderMessage}`, kept: { file: kept.file, order: kept.logical.order!, }, dropped: { file: dropped.file, order: dropped.logical.order!, }, } diagnostics.push(diagnostic) continue } // Fix - ignore the index const [index, literal] = routeIsFromIndexFile(route) ? [route, seenRoute] : [seenRoute, route] if (seenRoute === index) { seen.set(route) } // Report const diagnostic: DiagnosticIndexConflict = { type: `index-conflict`, severity: `error`, // dprint-ignore message: `Your files represent conflicting routes. This index file route:\n ${Path.format(index.file.path.relative)}\n\nconflicts with this literal file route:\n\n ${Path.format(literal.file.path.relative)}.\n\nYour index route is being ignored.`, literal: { file: literal.file, }, index: { file: index.file, }, } diagnostics.push(diagnostic) continue } // Continue seen.set(route) } return { diagnostics, routes: seen.toArray(), } }