next.js/packages/next/src/shared/lib/router/utils/route-regex.test.ts
route-regex.test.ts1266 lines35.7 KB
import { getNamedRouteRegex } from './route-regex'
import { parseParameter } from './get-dynamic-param'
import { pathToRegexp } from 'next/dist/compiled/path-to-regexp'

/**
 * Helper function to compile a pathToRegexpPattern from a route and test it against paths
 */
function compilePattern(
  route: string,
  options: Parameters<typeof getNamedRouteRegex>[1]
) {
  const regex = getNamedRouteRegex(route, options)

  const compiled = pathToRegexp(regex.pathToRegexpPattern, [], {
    strict: true,
    sensitive: false,
    delimiter: '/',
  })

  return { regex, compiled }
}

describe('getNamedRouteRegex', () => {
  it('should handle interception markers adjacent to dynamic path segments', () => {
    const regex = getNamedRouteRegex('/photos/(.)[author]/[id]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "author": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/photos/\\(\\.\\)(?<nxtIauthor>[^/]+?)/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/photos/(.):nxtIauthor/:nxtPid",
       "re": /\\^\\\\/photos\\\\/\\\\\\(\\\\\\.\\\\\\)\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {
           "author": "(.)",
         },
         "names": {
           "author": "nxtIauthor",
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtIauthor": "nxtIauthor",
         "nxtPid": "nxtPid",
       },
     }
    `)

    expect(regex.re.exec('/photos/(.)next/123')).toMatchInlineSnapshot(`
     [
       "/photos/(.)next/123",
       "next",
       "123",
     ]
    `)
  })

  it('should match named routes correctly when interception markers are adjacent to dynamic segments', () => {
    let regex = getNamedRouteRegex('/(.)[author]/[id]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "author": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/\\(\\.\\)(?<nxtIauthor>[^/]+?)/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/(.):nxtIauthor/:nxtPid",
       "re": /\\^\\\\/\\\\\\(\\\\\\.\\\\\\)\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {
           "author": "(.)",
         },
         "names": {
           "author": "nxtIauthor",
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtIauthor": "nxtIauthor",
         "nxtPid": "nxtPid",
       },
     }
    `)

    let namedRegexp = new RegExp(regex.namedRegex)
    expect(namedRegexp.test('/[author]/[id]')).toBe(false)
    expect(namedRegexp.test('/(.)[author]/[id]')).toBe(true)

    regex = getNamedRouteRegex('/(..)(..)[author]/[id]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "author": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/\\(\\.\\.\\)\\(\\.\\.\\)(?<nxtIauthor>[^/]+?)/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/(..)(..):nxtIauthor/:nxtPid",
       "re": /\\^\\\\/\\\\\\(\\\\\\.\\\\\\.\\\\\\)\\\\\\(\\\\\\.\\\\\\.\\\\\\)\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {
           "author": "(..)(..)",
         },
         "names": {
           "author": "nxtIauthor",
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtIauthor": "nxtIauthor",
         "nxtPid": "nxtPid",
       },
     }
    `)

    namedRegexp = new RegExp(regex.namedRegex)
    expect(namedRegexp.test('/[author]/[id]')).toBe(false)
    expect(namedRegexp.test('/(..)(..)[author]/[id]')).toBe(true)
  })

  it('should handle multi-level interception markers', () => {
    const regex = getNamedRouteRegex('/photos/(..)(..)[author]/[id]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "author": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/photos/\\(\\.\\.\\)\\(\\.\\.\\)(?<nxtIauthor>[^/]+?)/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/photos/(..)(..):nxtIauthor/:nxtPid",
       "re": /\\^\\\\/photos\\\\/\\\\\\(\\\\\\.\\\\\\.\\\\\\)\\\\\\(\\\\\\.\\\\\\.\\\\\\)\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {
           "author": "(..)(..)",
         },
         "names": {
           "author": "nxtIauthor",
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtIauthor": "nxtIauthor",
         "nxtPid": "nxtPid",
       },
     }
    `)

    expect(regex.re.exec('/photos/(..)(..)next/123')).toMatchInlineSnapshot(`
     [
       "/photos/(..)(..)next/123",
       "next",
       "123",
     ]
    `)
  })

  it('should not remove extra parts beside the param segments', () => {
    const regex = getNamedRouteRegex(
      '/[locale]/about.segments/[...segmentPath].segment.rsc',
      {
        prefixRouteKeys: true,
        includeSuffix: true,
      }
    )

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "locale": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "segmentPath": {
           "optional": false,
           "pos": 2,
           "repeat": true,
         },
       },
       "namedRegex": "^/(?<nxtPlocale>[^/]+?)/about\\.segments/(?<nxtPsegmentPath>.+?)\\.segment\\.rsc(?:/)?$",
       "pathToRegexpPattern": "/:nxtPlocale/about.segments/:nxtPsegmentPath+.segment.rsc",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/about\\\\\\.segments\\\\/\\(\\.\\+\\?\\)\\\\\\.segment\\\\\\.rsc\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "locale": "nxtPlocale",
           "segmentPath": "nxtPsegmentPath",
         },
       },
       "routeKeys": {
         "nxtPlocale": "nxtPlocale",
         "nxtPsegmentPath": "nxtPsegmentPath",
       },
     }
    `)
  })

  it('should not remove extra parts in front of the param segments', () => {
    const regex = getNamedRouteRegex(
      '/[locale]/about.segments/$dname$d[name].segment.rsc',
      {
        prefixRouteKeys: true,
        includeSuffix: true,
        includePrefix: true,
      }
    )

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "locale": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "name": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPlocale>[^/]+?)/about\\.segments/\\$dname\\$d(?<nxtPname>[^/]+?)\\.segment\\.rsc(?:/)?$",
       "pathToRegexpPattern": "/:nxtPlocale/about.segments/$dname$d/:nxtPname.segment.rsc",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/about\\\\\\.segments\\\\/\\\\\\$dname\\\\\\$d\\(\\[\\^/\\]\\+\\?\\)\\\\\\.segment\\\\\\.rsc\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "locale": "nxtPlocale",
           "name": "nxtPname",
         },
       },
       "routeKeys": {
         "nxtPlocale": "nxtPlocale",
         "nxtPname": "nxtPname",
       },
     }
    `)

    expect('/en/about.segments/$dname$dwyatt.segment.rsc'.match(regex.re))
      .toMatchInlineSnapshot(`
     [
       "/en/about.segments/$dname$dwyatt.segment.rsc",
       "en",
       "wyatt",
     ]
    `)
  })

  it('should handle interception markers not adjacent to dynamic path segments', () => {
    const regex = getNamedRouteRegex('/photos/(.)author/[id]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/photos/\\(\\.\\)author/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/photos/(.)author/:nxtPid",
       "re": /\\^\\\\/photos\\\\/\\\\\\(\\\\\\.\\\\\\)author\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)

    expect(regex.re.exec('/photos/(.)author/123')).toMatchInlineSnapshot(`
     [
       "/photos/(.)author/123",
       "123",
     ]
    `)
  })

  it('should handle optional dynamic path segments', () => {
    const regex = getNamedRouteRegex('/photos/[[id]]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": true,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/photos(?:/(?<nxtPid>[^/]+?))?(?:/)?$",
       "pathToRegexpPattern": "/photos/:nxtPid",
       "re": /\\^\\\\/photos\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)

    expect(regex.routeKeys).toEqual({
      nxtPid: 'nxtPid',
    })

    expect(regex.groups['id']).toEqual({
      pos: 1,
      repeat: false,
      optional: true,
    })
  })

  it('should handle optional catch-all dynamic path segments', () => {
    const regex = getNamedRouteRegex('/photos/[[...id]]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": true,
           "pos": 1,
           "repeat": true,
         },
       },
       "namedRegex": "^/photos(?:/(?<nxtPid>.+?))?(?:/)?$",
       "pathToRegexpPattern": "/photos/:nxtPid*",
       "re": /\\^\\\\/photos\\(\\?:\\\\/\\(\\.\\+\\?\\)\\)\\?\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)

    expect(regex.re.exec('/photos/1')).toMatchInlineSnapshot(`
     [
       "/photos/1",
       "1",
     ]
    `)
    expect(regex.re.exec('/photos/1/2/3')).toMatchInlineSnapshot(`
     [
       "/photos/1/2/3",
       "1/2/3",
     ]
    `)
    expect(regex.re.exec('/photos')).toMatchInlineSnapshot(`
     [
       "/photos",
       undefined,
     ]
    `)
  })
})

describe('getNamedRouteRegex - Parameter Sanitization', () => {
  it('should sanitize parameter names with hyphens', () => {
    const regex = getNamedRouteRegex('/[foo-bar]/page', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "foo-bar": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPfoobar>[^/]+?)/page(?:/)?$",
       "pathToRegexpPattern": "/:nxtPfoobar/page",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/page\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "foo-bar": "nxtPfoobar",
         },
       },
       "routeKeys": {
         "nxtPfoobar": "nxtPfoo-bar",
       },
     }
    `)
  })

  it('should sanitize parameter names with underscores', () => {
    const regex = getNamedRouteRegex('/[foo_id]/page', {
      prefixRouteKeys: true,
    })

    // Underscores should be removed from parameter names
    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "foo_id": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPfoo_id>[^/]+?)/page(?:/)?$",
       "pathToRegexpPattern": "/:nxtPfoo_id/page",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/page\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "foo_id": "nxtPfoo_id",
         },
       },
       "routeKeys": {
         "nxtPfoo_id": "nxtPfoo_id",
       },
     }
    `)
  })

  it('should handle parameters with multiple special characters', () => {
    const regex = getNamedRouteRegex('/[this-is_my-route]/page', {
      prefixRouteKeys: true,
    })

    // Special characters are removed for the sanitized key, but routeKeys maps back to original
    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "this-is_my-route": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPthisis_myroute>[^/]+?)/page(?:/)?$",
       "pathToRegexpPattern": "/:nxtPthisis_myroute/page",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/page\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "this-is_my-route": "nxtPthisis_myroute",
         },
       },
       "routeKeys": {
         "nxtPthisis_myroute": "nxtPthis-is_my-route",
       },
     }
    `)
  })

  it('should generate safe keys for invalid parameter names', () => {
    // Parameter name that starts with a number gets the prefix but keeps numbers
    const regex1 = getNamedRouteRegex('/[123invalid]/page', {
      prefixRouteKeys: true,
    })

    // Numbers at the start cause fallback, but with prefix it becomes valid
    expect(Object.keys(regex1.routeKeys)).toHaveLength(1)
    const key1 = Object.keys(regex1.routeKeys)[0]
    // With prefixRouteKeys, the nxtP prefix makes it valid even with leading numbers
    expect(key1).toMatch(/^nxtP123invalid$/)

    // Parameter name that's too long (>30 chars) triggers fallback
    const longName = 'a'.repeat(35)
    const regex2 = getNamedRouteRegex(`/[${longName}]/page`, {
      prefixRouteKeys: true,
    })

    // Should fall back to generated safe key
    expect(Object.keys(regex2.routeKeys)).toHaveLength(1)
    const key2 = Object.keys(regex2.routeKeys)[0]
    // Fallback keys are just lowercase letters
    expect(key2).toMatch(/^[a-z]+$/)
    expect(key2.length).toBeLessThanOrEqual(30)
  })
})

describe('getNamedRouteRegex - Reference Mapping', () => {
  it('should use provided reference for parameter mapping', () => {
    // First call establishes the reference
    const regex1 = getNamedRouteRegex('/[lang]/photos', {
      prefixRouteKeys: true,
    })

    // Second call uses the reference from the first
    const regex2 = getNamedRouteRegex('/[lang]/photos/[id]', {
      prefixRouteKeys: true,
      reference: regex1.reference,
    })

    // Both should use the same prefixed key for 'lang'
    expect(regex1.reference.names.lang).toBe(regex2.reference.names.lang)
    expect(regex2.reference.names.lang).toBe('nxtPlang')

    // New parameter should be added to the reference
    expect(regex2.reference.names.id).toBe('nxtPid')
  })

  it('should maintain reference consistency across multiple paths', () => {
    const baseRegex = getNamedRouteRegex('/[locale]/example', {
      prefixRouteKeys: true,
    })

    const interceptedRegex = getNamedRouteRegex('/[locale]/intercepted', {
      prefixRouteKeys: true,
      reference: baseRegex.reference,
    })

    // Same parameter name should map to same prefixed key
    expect(baseRegex.reference.names.locale).toBe(
      interceptedRegex.reference.names.locale
    )
    expect(interceptedRegex.reference.names.locale).toBe('nxtPlocale')
  })

  it('should generate inverse pattern with correct parameter references', () => {
    const regex = getNamedRouteRegex('/[lang]/posts/[id]', {
      prefixRouteKeys: true,
    })

    // Inverse pattern should use the same prefixed keys
    expect(regex.pathToRegexpPattern).toBe('/:nxtPlang/posts/:nxtPid')

    // And they should match the routeKeys
    expect(regex.routeKeys.nxtPlang).toBe('nxtPlang')
    expect(regex.routeKeys.nxtPid).toBe('nxtPid')
  })
})

describe('getNamedRouteRegex - Duplicate Keys', () => {
  it('should handle duplicate parameters with backreferences', () => {
    const regex = getNamedRouteRegex('/[id]/posts/[id]', {
      prefixRouteKeys: true,
      backreferenceDuplicateKeys: true,
    })

    // Should have only one key, named regex should contain a backreference for
    // the second occurrence
    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPid>[^/]+?)/posts/\\k<nxtPid>(?:/)?$",
       "pathToRegexpPattern": "/:nxtPid/posts/:nxtPid",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/posts\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)
  })

  it('should handle duplicate parameters without backreferences', () => {
    const regex = getNamedRouteRegex('/[id]/posts/[id]', {
      prefixRouteKeys: true,
      backreferenceDuplicateKeys: false,
    })

    // Should still have only one key, but no backreference in the pattern.
    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPid>[^/]+?)/posts/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/:nxtPid/posts/:nxtPid",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/posts\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)
  })
})

describe('getNamedRouteRegex - Complex Paths', () => {
  it('should handle paths with multiple dynamic segments', () => {
    const regex = getNamedRouteRegex('/[org]/[repo]/[branch]/[...path]', {
      prefixRouteKeys: true,
    })

    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "branch": {
           "optional": false,
           "pos": 3,
           "repeat": false,
         },
         "org": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
         "path": {
           "optional": false,
           "pos": 4,
           "repeat": true,
         },
         "repo": {
           "optional": false,
           "pos": 2,
           "repeat": false,
         },
       },
       "namedRegex": "^/(?<nxtPorg>[^/]+?)/(?<nxtPrepo>[^/]+?)/(?<nxtPbranch>[^/]+?)/(?<nxtPpath>.+?)(?:/)?$",
       "pathToRegexpPattern": "/:nxtPorg/:nxtPrepo/:nxtPbranch/:nxtPpath+",
       "re": /\\^\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\[\\^/\\]\\+\\?\\)\\\\/\\(\\.\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "branch": "nxtPbranch",
           "org": "nxtPorg",
           "path": "nxtPpath",
           "repo": "nxtPrepo",
         },
       },
       "routeKeys": {
         "nxtPbranch": "nxtPbranch",
         "nxtPorg": "nxtPorg",
         "nxtPpath": "nxtPpath",
         "nxtPrepo": "nxtPrepo",
       },
     }
    `)

    // Test actual matching
    const match = regex.re.exec('/vercel/next.js/canary/docs/api/reference')
    expect(match).toBeTruthy()
    expect(match![0]).toBe('/vercel/next.js/canary/docs/api/reference')
    expect(match![1]).toBe('vercel')
    expect(match![2]).toBe('next.js')
    expect(match![3]).toBe('canary')
    expect(match![4]).toBe('docs/api/reference')
  })

  it('should mark optional segments correctly', () => {
    // Optional segments are marked as optional in the groups
    const regex = getNamedRouteRegex('/posts/[[slug]]', {
      prefixRouteKeys: true,
    })

    expect(regex.routeKeys).toEqual({
      nxtPslug: 'nxtPslug',
    })

    expect(regex.groups).toEqual({
      slug: { pos: 1, repeat: false, optional: true },
    })

    // Regex should include optional pattern
    expect(regex.namedRegex).toContain('?')
  })

  it('should handle all interception markers', () => {
    const markers = ['(.)', '(..)', '(..)(..)', '(...)']

    for (const marker of markers) {
      const regex = getNamedRouteRegex(`/photos/${marker}[id]`, {
        prefixRouteKeys: true,
      })

      // Should use consistent parameter prefix (interception marker adjacent to parameter uses nxtI)
      expect(regex.routeKeys).toEqual({
        nxtIid: 'nxtIid',
      })

      // Should escape the marker in the regex
      const escapedMarker = marker.replace(/[().]/g, '\\$&')
      expect(regex.namedRegex).toContain(escapedMarker)
    }
  })
})

describe('getNamedRouteRegex - Trailing Slash Behavior', () => {
  it('should include optional trailing slash by default', () => {
    const regex = getNamedRouteRegex('/posts/[id]', {
      prefixRouteKeys: true,
    })

    // Should end with optional trailing slash
    expect(regex).toMatchInlineSnapshot(`
     {
       "groups": {
         "id": {
           "optional": false,
           "pos": 1,
           "repeat": false,
         },
       },
       "namedRegex": "^/posts/(?<nxtPid>[^/]+?)(?:/)?$",
       "pathToRegexpPattern": "/posts/:nxtPid",
       "re": /\\^\\\\/posts\\\\/\\(\\[\\^/\\]\\+\\?\\)\\(\\?:\\\\/\\)\\?\\$/,
       "reference": {
         "intercepted": {},
         "names": {
           "id": "nxtPid",
         },
       },
       "routeKeys": {
         "nxtPid": "nxtPid",
       },
     }
    `)

    // Should match both with and without trailing slash
    const namedRe = new RegExp(regex.namedRegex)
    expect(namedRe.test('/posts/123')).toBe(true)
    expect(namedRe.test('/posts/123/')).toBe(true)
  })

  it('should exclude optional trailing slash when specified', () => {
    const regex = getNamedRouteRegex('/posts/[id]', {
      prefixRouteKeys: true,
      excludeOptionalTrailingSlash: true,
    })

    // Should NOT have optional trailing slash
    expect(regex.namedRegex).not.toMatch(/\(\?:\/\)\?\$/)
    expect(regex.namedRegex).toMatch(/\$/)

    // Should still match without trailing slash
    const namedRe = new RegExp(regex.namedRegex)
    expect(namedRe.test('/posts/123')).toBe(true)
  })
})

describe('getNamedRouteRegex - Edge Cases', () => {
  it('should handle root route', () => {
    const regex = getNamedRouteRegex('/', {
      prefixRouteKeys: true,
    })

    expect(regex.routeKeys).toEqual({})
    expect(regex.groups).toEqual({})
    expect(regex.namedRegex).toMatch(/^\^\//)
  })

  it('should handle route with only interception marker', () => {
    const regex = getNamedRouteRegex('/(.)nested', {
      prefixRouteKeys: true,
    })

    // No dynamic segments
    expect(regex.routeKeys).toEqual({})

    // Should escape the marker
    expect(regex.namedRegex).toContain('\\(\\.\\)')
  })

  it('should handle interception marker followed by catchall segment', () => {
    // Interception marker must be followed by a segment name, then catchall
    const regex = getNamedRouteRegex('/photos/(.)images/[...path]', {
      prefixRouteKeys: true,
    })

    expect(regex.routeKeys).toEqual({
      nxtPpath: 'nxtPpath',
    })

    expect(regex.groups.path).toEqual({
      pos: 1,
      repeat: true,
      optional: false,
    })

    // Should match multiple segments after the static segment
    expect(regex.re.test('/photos/(.)images/a')).toBe(true)
    expect(regex.re.test('/photos/(.)images/a/b/c')).toBe(true)
  })

  it('should handle dynamic segment with interception marker prefix', () => {
    // Interception marker can be adjacent to dynamic segment
    const regex = getNamedRouteRegex('/photos/(.)[id]', {
      prefixRouteKeys: true,
    })

    expect(regex.routeKeys).toEqual({
      nxtIid: 'nxtIid',
    })

    expect(regex.groups.id).toEqual({
      pos: 1,
      repeat: false,
      optional: false,
    })

    // Should match single segment after the marker
    expect(regex.re.test('/photos/(.)123')).toBe(true)
  })

  it('should handle prefix and suffix options together', () => {
    const regex = getNamedRouteRegex('/api.v1/users.$type$[id].json', {
      prefixRouteKeys: true,
      includePrefix: true,
      includeSuffix: true,
    })

    // Should preserve prefix and suffix in regex
    expect(regex.namedRegex).toContain('\\$type\\$')
    expect(regex.namedRegex).toContain('\\.json')

    // Test matching
    const namedRe = new RegExp(regex.namedRegex)
    expect(namedRe.test('/api.v1/users.$type$123.json')).toBe(true)
  })

  it('should generate correct inverse pattern for complex routes', () => {
    const regex = getNamedRouteRegex('/[org]/@modal/(..)photo/[id]', {
      prefixRouteKeys: true,
    })

    // When interception marker is not adjacent to a parameter, the [id] uses regular prefix
    expect(regex.pathToRegexpPattern).toBe('/:nxtPorg/@modal/(..)photo/:nxtPid')

    // routeKeys should have both parameters with appropriate prefixes
    expect(regex.routeKeys).toEqual({
      nxtPorg: 'nxtPorg',
      nxtPid: 'nxtPid',
    })
  })

  it('should handle path with multiple separate segments', () => {
    // Dynamic segments need to be separated by slashes
    const regex = getNamedRouteRegex('/[org]/[repo]/[branch]', {
      prefixRouteKeys: true,
    })

    expect(regex.routeKeys).toEqual({
      nxtPorg: 'nxtPorg',
      nxtPrepo: 'nxtPrepo',
      nxtPbranch: 'nxtPbranch',
    })

    // Each segment is captured separately
    const match = regex.re.exec('/vercel/next.js/canary')
    expect(match).toBeTruthy()
    expect(match![1]).toBe('vercel')
    expect(match![2]).toBe('next.js')
    expect(match![3]).toBe('canary')
  })
})

describe('getNamedRouteRegex - Named Capture Groups', () => {
  it('should extract values using named capture groups', () => {
    const regex = getNamedRouteRegex('/posts/[category]/[id]', {
      prefixRouteKeys: true,
    })

    const namedRe = new RegExp(regex.namedRegex)
    const match = namedRe.exec('/posts/tech/123')

    expect(match).toBeTruthy()
    expect(match?.groups).toEqual({
      nxtPcategory: 'tech',
      nxtPid: '123',
    })
  })

  it('should extract values with interception markers', () => {
    const regex = getNamedRouteRegex('/photos/(.)[author]/[id]', {
      prefixRouteKeys: true,
    })

    const namedRe = new RegExp(regex.namedRegex)
    const match = namedRe.exec('/photos/(.)john/123')

    expect(match).toBeTruthy()
    expect(match?.groups).toEqual({
      nxtIauthor: 'john',
      nxtPid: '123',
    })
  })

  it('should extract catchall values correctly', () => {
    const regex = getNamedRouteRegex('/files/[...path]', {
      prefixRouteKeys: true,
    })

    const namedRe = new RegExp(regex.namedRegex)
    const match = namedRe.exec('/files/docs/api/reference.md')

    expect(match).toBeTruthy()
    expect(match?.groups).toEqual({
      nxtPpath: 'docs/api/reference.md',
    })
  })
})

describe('parseParameter', () => {
  it('should parse a optional catchall parameter', () => {
    const param = '[[...slug]]'
    const expected = { key: 'slug', repeat: true, optional: true }
    const result = parseParameter(param)
    expect(result).toEqual(expected)
  })

  it('should parse a catchall parameter', () => {
    const param = '[...slug]'
    const expected = { key: 'slug', repeat: true, optional: false }
    const result = parseParameter(param)
    expect(result).toEqual(expected)
  })

  it('should parse a optional parameter', () => {
    const param = '[[foo]]'
    const expected = { key: 'foo', repeat: false, optional: true }
    const result = parseParameter(param)
    expect(result).toEqual(expected)
  })

  it('should parse a dynamic parameter', () => {
    const param = '[bar]'
    const expected = { key: 'bar', repeat: false, optional: false }
    const result = parseParameter(param)
    expect(result).toEqual(expected)
  })

  it('should parse non-dynamic parameter', () => {
    const param = 'fizz'
    const expected = { key: 'fizz', repeat: false, optional: false }
    const result = parseParameter(param)
    expect(result).toEqual(expected)
  })
})

describe('getNamedRouteRegex - pathToRegexpPattern Conformance', () => {
  describe('Basic Patterns', () => {
    it('should generate a pattern that matches single dynamic segment routes', () => {
      const { regex, compiled } = compilePattern('/posts/[id]', {
        prefixRouteKeys: true,
      })

      // Verify the pattern format
      expect(regex.pathToRegexpPattern).toBe('/posts/:nxtPid')

      // Should match valid routes
      expect(compiled.exec('/posts/123')).toMatchInlineSnapshot(`
        [
          "/posts/123",
          "123",
        ]
      `)
      expect(compiled.exec('/posts/abc-def')).toMatchInlineSnapshot(`
        [
          "/posts/abc-def",
          "abc-def",
        ]
      `)

      // Should not match invalid routes
      expect(compiled.exec('/posts')).toBe(null)
      expect(compiled.exec('/posts/123/extra')).toBe(null)
    })

    it('should generate a pattern that matches multiple dynamic segment routes', () => {
      const { regex, compiled } = compilePattern('/[org]/[repo]/[branch]', {
        prefixRouteKeys: true,
      })

      // Verify the pattern format
      expect(regex.pathToRegexpPattern).toBe('/:nxtPorg/:nxtPrepo/:nxtPbranch')

      // Should match valid routes
      expect(compiled.exec('/vercel/next.js/canary')).toMatchInlineSnapshot(`
        [
          "/vercel/next.js/canary",
          "vercel",
          "next.js",
          "canary",
        ]
      `)

      // Should not match incomplete routes
      expect(compiled.exec('/vercel')).toBe(null)
      expect(compiled.exec('/vercel/next.js')).toBe(null)
    })
  })

  describe('Catch-all Segments', () => {
    it('should generate a pattern for required catch-all segments', () => {
      const { regex, compiled } = compilePattern('/files/[...path]', {
        prefixRouteKeys: true,
      })

      // Verify the pattern uses the + modifier for required catch-all
      expect(regex.pathToRegexpPattern).toBe('/files/:nxtPpath+')

      // Should match single segments
      expect(compiled.exec('/files/a')).toMatchInlineSnapshot(`
        [
          "/files/a",
          "a",
        ]
      `)

      // Should match multiple segments
      expect(compiled.exec('/files/a/b/c')).toMatchInlineSnapshot(`
        [
          "/files/a/b/c",
          "a/b/c",
        ]
      `)

      // Should not match without any segments
      expect(compiled.exec('/files')).toBe(null)
    })

    it('should generate a pattern for optional catch-all segments', () => {
      const { regex, compiled } = compilePattern('/photos/[[...id]]', {
        prefixRouteKeys: true,
      })

      // Verify the pattern uses the * modifier for optional catch-all
      expect(regex.pathToRegexpPattern).toBe('/photos/:nxtPid*')

      // Should match without segments
      expect(compiled.exec('/photos')).toMatchInlineSnapshot(`
        [
          "/photos",
          undefined,
        ]
      `)

      // Should match single segment
      expect(compiled.exec('/photos/1')).toMatchInlineSnapshot(`
        [
          "/photos/1",
          "1",
        ]
      `)

      // Should match multiple segments
      expect(compiled.exec('/photos/1/2/3')).toMatchInlineSnapshot(`
        [
          "/photos/1/2/3",
          "1/2/3",
        ]
      `)
    })

    it('should generate a pattern for catch-all after static segments', () => {
      const { regex, compiled } = compilePattern('/docs/api/[...slug]', {
        prefixRouteKeys: true,
      })

      expect(regex.pathToRegexpPattern).toBe('/docs/api/:nxtPslug+')

      expect(compiled.exec('/docs/api/reference')).toMatchInlineSnapshot(`
        [
          "/docs/api/reference",
          "reference",
        ]
      `)
      expect(compiled.exec('/docs/api/v1/users/create')).toMatchInlineSnapshot(`
        [
          "/docs/api/v1/users/create",
          "v1/users/create",
        ]
      `)

      // Should not match without the catch-all segment
      expect(compiled.exec('/docs/api')).toBe(null)
    })
  })

  describe('Optional Segments', () => {
    it('should generate a pattern for optional single segments', () => {
      const { regex, compiled } = compilePattern('/photos/[[id]]', {
        prefixRouteKeys: true,
      })

      // Verify the pattern format for optional segments
      expect(regex.pathToRegexpPattern).toBe('/photos/:nxtPid')

      // Should match with the segment
      expect(compiled.exec('/photos/123')).toMatchInlineSnapshot(`
        [
          "/photos/123",
          "123",
        ]
      `)

      // Should match without the segment (note: path-to-regexp behavior)
      // The pattern generated doesn't include a modifier, so this might not match
      // This test verifies the actual behavior
      const withoutSegment = compiled.exec('/photos')
      expect(withoutSegment).toBe(null)
    })

    it('should generate a pattern for multiple optional segments', () => {
      const { regex, compiled } = compilePattern('/posts/[[category]]/[[id]]', {
        prefixRouteKeys: true,
      })

      expect(regex.pathToRegexpPattern).toBe('/posts/:nxtPcategory/:nxtPid')

      // Should match with all segments
      expect(compiled.exec('/posts/tech/123')).toMatchInlineSnapshot(`
        [
          "/posts/tech/123",
          "tech",
          "123",
        ]
      `)

      // Note: The pattern generated doesn't have optional modifiers,
      // so it requires all segments to be present
      expect(compiled.exec('/posts/tech')).toBe(null)
      expect(compiled.exec('/posts')).toBe(null)
    })
  })

  describe('Complex Patterns', () => {
    it('should generate a pattern for routes with prefixes and suffixes', () => {
      const route = '/[locale]/about.segments/$dname$d[name].segment.rsc'
      const regex = getNamedRouteRegex(route, {
        prefixRouteKeys: true,
        includeSuffix: true,
        includePrefix: true,
      })

      expect(regex.pathToRegexpPattern).toBe(
        '/:nxtPlocale/about.segments/$dname$d/:nxtPname.segment.rsc'
      )

      // For this complex pattern with special chars, verify the pattern format
      // but don't test compilation since path-to-regexp may not handle all edge cases
      // The important part is that pathToRegexpPattern is generated correctly
    })

    it('should generate a pattern for routes with catch-all and static segments', () => {
      const { regex, compiled } = compilePattern(
        '/[locale]/docs/v2/[...slug]',
        {
          prefixRouteKeys: true,
        }
      )

      expect(regex.pathToRegexpPattern).toBe('/:nxtPlocale/docs/v2/:nxtPslug+')

      expect(compiled.exec('/en/docs/v2/api/reference')).toMatchInlineSnapshot(`
        [
          "/en/docs/v2/api/reference",
          "en",
          "api/reference",
        ]
      `)

      // Should not match without locale
      expect(compiled.exec('/docs/v2/api/reference')).toBe(null)

      // Should not match without catch-all
      expect(compiled.exec('/en/docs/v2')).toBe(null)
    })

    it('should generate a pattern for deeply nested dynamic routes', () => {
      const { regex, compiled } = compilePattern(
        '/[org]/[repo]/[branch]/[...path]',
        {
          prefixRouteKeys: true,
        }
      )

      expect(regex.pathToRegexpPattern).toBe(
        '/:nxtPorg/:nxtPrepo/:nxtPbranch/:nxtPpath+'
      )

      expect(compiled.exec('/vercel/next.js/canary/docs/api/reference.md'))
        .toMatchInlineSnapshot(`
        [
          "/vercel/next.js/canary/docs/api/reference.md",
          "vercel",
          "next.js",
          "canary",
          "docs/api/reference.md",
        ]
      `)
    })
  })
})
Quest for Codev2.0.0
/
SIGN IN