Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bugfix: route interception with dynamic paths #51526

Merged
merged 12 commits into from
Jun 22, 2023
39 changes: 32 additions & 7 deletions packages/next/src/shared/lib/router/utils/route-regex.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { getRouteRegex } from './route-regex'
import { getNamedRouteRegex } from './route-regex'

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

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

expect(regex.groups['author']).toEqual({
pos: 1,
repeat: false,
Expand All @@ -19,7 +25,13 @@ describe('getRouteRegex', () => {
})

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

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

expect(regex.groups['author']).toEqual({
pos: 1,
repeat: false,
Expand All @@ -36,9 +48,14 @@ describe('getRouteRegex', () => {
})

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

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

expect(regex.groups['author']).toBeUndefined()

expect(regex.groups['id']).toEqual({
pos: 1,
repeat: false,
Expand All @@ -49,7 +66,11 @@ describe('getRouteRegex', () => {
})

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

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

expect(regex.groups['id']).toEqual({
pos: 1,
Expand All @@ -59,7 +80,11 @@ describe('getRouteRegex', () => {
})

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

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

expect(regex.groups['id']).toEqual({
pos: 1,
Expand Down
102 changes: 66 additions & 36 deletions packages/next/src/shared/lib/router/utils/route-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { escapeStringRegexp } from '../../escape-regexp'
import { removeTrailingSlash } from './remove-trailing-slash'

const NEXT_QUERY_PARAM_PREFIX = 'nxtP'
const NEXT_INTERCEPTION_MARKER_PREFIX = 'nxtI'

export interface Group {
pos: number
Expand Down Expand Up @@ -99,48 +100,77 @@ function buildGetSafeRouteKey() {
}
}

function getSafeKeyFromSegment({
segment,
routeKeys,
keyPrefix,
}: {
segment: string
routeKeys: Record<string, string>
keyPrefix?: string
}) {
const getSafeRouteKey = buildGetSafeRouteKey()

const { key, optional, repeat } = parseParameter(segment)

// replace any non-word characters since they can break
// the named regex
let cleanedKey = key.replace(/\W/g, '')

if (keyPrefix) {
cleanedKey = `${keyPrefix}${cleanedKey}`
}
let invalidKey = false

// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true
}
if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) {
invalidKey = true
}

if (invalidKey) {
cleanedKey = getSafeRouteKey()
}

if (keyPrefix) {
routeKeys[cleanedKey] = `${keyPrefix}${key}`
} else {
routeKeys[cleanedKey] = `${key}`
}

return repeat
? optional
? `(?:/(?<${cleanedKey}>.+?))?`
: `/(?<${cleanedKey}>.+?)`
: `/(?<${cleanedKey}>[^/]+?)`
}

function getNamedParametrizedRoute(route: string, prefixRouteKeys: boolean) {
const segments = removeTrailingSlash(route).slice(1).split('/')
const getSafeRouteKey = buildGetSafeRouteKey()
const routeKeys: { [named: string]: string } = {}
return {
namedParameterizedRoute: segments
.map((segment) => {
if (segment.startsWith('[') && segment.endsWith(']')) {
const { key, optional, repeat } = parseParameter(segment.slice(1, -1))
// replace any non-word characters since they can break
// the named regex
let cleanedKey = key.replace(/\W/g, '')

if (prefixRouteKeys) {
cleanedKey = `${NEXT_QUERY_PARAM_PREFIX}${cleanedKey}`
}
let invalidKey = false

// check if the key is still invalid and fallback to using a known
// safe key
if (cleanedKey.length === 0 || cleanedKey.length > 30) {
invalidKey = true
}
if (!isNaN(parseInt(cleanedKey.slice(0, 1)))) {
invalidKey = true
}

if (invalidKey) {
cleanedKey = getSafeRouteKey()
}

if (prefixRouteKeys) {
routeKeys[cleanedKey] = `${NEXT_QUERY_PARAM_PREFIX}${key}`
} else {
routeKeys[cleanedKey] = `${key}`
}

return repeat
? optional
? `(?:/(?<${cleanedKey}>.+?))?`
: `/(?<${cleanedKey}>.+?)`
: `/(?<${cleanedKey}>[^/]+?)`
const markerMatches = segment.match(/\((\.{1,3})\)(\w*)/) // Check for intercept markers
ztanner marked this conversation as resolved.
Show resolved Hide resolved
const paramMatches = segment.match(/\[((?:\[.*\])|.+)\]/) // Check for parameters

if (markerMatches && paramMatches) {
return getSafeKeyFromSegment({
segment: paramMatches[1],
routeKeys,
keyPrefix: prefixRouteKeys
? NEXT_INTERCEPTION_MARKER_PREFIX
: undefined,
})
} else if (paramMatches) {
return getSafeKeyFromSegment({
segment: paramMatches[1],
routeKeys,
keyPrefix: prefixRouteKeys ? NEXT_QUERY_PARAM_PREFIX : undefined,
})
} else {
return `/${escapeStringRegexp(segment)}`
}
Expand Down