@@ -348,13 +348,30 @@ export async function bundle (nuxt: Nuxt & { _nitro?: Nitro }): Promise<void> {
348348
349349 const validManifestKeys = [ 'prerender' , 'redirect' , 'appMiddleware' , 'appLayout' , 'cache' , 'isr' , 'swr' , 'ssr' ]
350350
351- function getRouteRulesRouter ( ) {
351+ // rou3 matches keys case-sensitively, but vue-router matches routes case-insensitively
352+ // unless `sensitive`, so an insensitive-routing rule keyed `/Admin` would never match a
353+ // folded lookup and silently lose its protections. `sensitive` can also come from
354+ // `app/router.options.ts` (runtime-only), so emit both a verbatim and a folded matcher
355+ // and pick at runtime.
356+ const caseSensitiveRouteRules = ! ! nuxt . options . router . options . sensitive
357+ const foldRouteRuleKey = ( route : string ) => caseSensitiveRouteRules || typeof route !== 'string' ? route : route . toLowerCase ( )
358+
359+ function getRouteRulesRouter ( fold : boolean ) {
352360 const routeRulesRouter = createRou3Router < NitroRouteRules > ( )
353361 if ( nuxt . _nitro ) {
362+ const foldedKeys = new Map < string , string > ( )
354363 for ( const [ route , rules ] of Object . entries ( nuxt . _nitro . options . routeRules ) ) {
355364 if ( route === '/__nuxt_error' ) { continue }
356365 if ( validManifestKeys . every ( key => ! ( key in rules ) ) ) { continue }
357- addRoute ( routeRulesRouter , undefined , route , rules )
366+ const key = fold && typeof route === 'string' ? route . toLowerCase ( ) : route
367+ if ( fold ) {
368+ const existing = foldedKeys . get ( key )
369+ if ( existing !== undefined && existing !== route && ! caseSensitiveRouteRules ) {
370+ logger . warn ( `Route rules for \`${ existing } \` and \`${ route } \` resolve to the same path when matched case-insensitively; \`${ route } \` takes precedence. Disambiguate the keys or set \`router.options.sensitive: true\`.` )
371+ }
372+ foldedKeys . set ( key , route )
373+ }
374+ addRoute ( routeRulesRouter , undefined , key , rules )
358375 }
359376 }
360377 return routeRulesRouter
@@ -368,7 +385,7 @@ export async function bundle (nuxt: Nuxt & { _nitro?: Nitro }): Promise<void> {
368385 if ( cachedMatchers [ key ] ) {
369386 return cachedMatchers [ key ]
370387 }
371- const matcher = compileRouterToString ( getRouteRulesRouter ( ) , '' , {
388+ const compile = ( fold : boolean ) => compileRouterToString ( getRouteRulesRouter ( fold ) , '' , {
372389 matchAll : true ,
373390 serialize ( routeRules ) {
374391 return `{${ Object . entries ( routeRules )
@@ -399,11 +416,17 @@ export async function bundle (nuxt: Nuxt & { _nitro?: Nitro }): Promise<void> {
399416 } }`
400417 } ,
401418 } )
402- return cachedMatchers [ key ] = `
403- import { defu } from 'defu'
404- const matcher = ${ matcher }
405- export default (path) => defu({}, ...matcher('', typeof path === 'string' ? path.toLowerCase() : path).map(r => r.data).reverse())
406- `
419+ const sensitiveMatcher = compile ( false )
420+ const foldedMatcher = compile ( true )
421+ return cachedMatchers [ key ] = [
422+ `import { defu } from 'defu'` ,
423+ `import routerOptions from '#build/router.options.mjs'` ,
424+ `const sensitiveMatcher = ${ sensitiveMatcher } ` ,
425+ foldedMatcher === sensitiveMatcher ? `const foldedMatcher = sensitiveMatcher` : `const foldedMatcher = ${ foldedMatcher } ` ,
426+ `export default (path) => routerOptions.sensitive` ,
427+ ` ? defu({}, ...sensitiveMatcher('', path).map(r => r.data).reverse())` ,
428+ ` : defu({}, ...foldedMatcher('', typeof path === 'string' ? path.toLowerCase() : path).map(r => r.data).reverse())` ,
429+ ] . join ( '\n' )
407430 } ,
408431 } )
409432
@@ -500,13 +523,13 @@ export async function bundle (nuxt: Nuxt & { _nitro?: Nitro }): Promise<void> {
500523 nitro . hooks . hook ( 'rollup:before' , async ( nitro ) => {
501524 // Add pages prerendered but not covered by route rules
502525 const prerenderedRoutes = new Set < string > ( )
503- const routeRulesMatcher = getRouteRulesRouter ( )
526+ const routeRulesMatcher = getRouteRulesRouter ( ! caseSensitiveRouteRules )
504527 if ( nitro . _prerenderedRoutes ?. length ) {
505528 const payloadSuffix = nuxt . options . experimental . renderJsonPayloads ? '/_payload.json' : '/_payload.js'
506529 for ( const route of nitro . _prerenderedRoutes ) {
507530 if ( ! route . error && route . route . endsWith ( payloadSuffix ) ) {
508531 const url = route . route . slice ( 0 , - payloadSuffix . length ) || '/'
509- const rules = defu ( { } , ...findAllRoutes ( routeRulesMatcher , undefined , url ) . reverse ( ) ) as Record < string , any >
532+ const rules = defu ( { } , ...findAllRoutes ( routeRulesMatcher , undefined , foldRouteRuleKey ( url ) ) . reverse ( ) ) as Record < string , any >
510533 if ( ! rules . prerender ) {
511534 prerenderedRoutes . add ( url )
512535 }
0 commit comments