Skip to content

Commit

Permalink
Fix interpolation of dynamic params in intercepting route pathnames
Browse files Browse the repository at this point in the history
In a pathname for an intercepting route, a dynamic path param might
occur multiple types, e.g.
`'/[locale]/example/(...)[locale]/intercepted'`. We need to make sure
that each occurence is replaced with the actual value during
interpolation.

The handling of such pathnames is already done correctly in the Node.js
runtime, but not in the Edge runtime.

fixes #70654
  • Loading branch information
unstubbable committed Oct 1, 2024
1 parent eecf90c commit 4f9a3f4
Show file tree
Hide file tree
Showing 11 changed files with 130 additions and 17 deletions.
27 changes: 10 additions & 17 deletions packages/next/src/server/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,18 @@ export function interpolateDynamicPath(
builtParam = `[${builtParam}]`
}

const paramIdx = pathname!.indexOf(builtParam)

if (paramIdx > -1) {
let paramValue: string
const value = params[param]

if (Array.isArray(value)) {
paramValue = value.map((v) => v && encodeURIComponent(v)).join('/')
} else if (value) {
paramValue = encodeURIComponent(value)
} else {
paramValue = ''
}
let paramValue: string
const value = params[param]

pathname =
pathname.slice(0, paramIdx) +
paramValue +
pathname.slice(paramIdx + builtParam.length)
if (Array.isArray(value)) {
paramValue = value.map((v) => v && encodeURIComponent(v)).join('/')
} else if (value) {
paramValue = encodeURIComponent(value)
} else {
paramValue = ''
}

pathname = pathname.replaceAll(builtParam, paramValue)
}

return pathname
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<h2>Page intercepted from root</h2>
<Link href="/en/example">Back to /en/example</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Default() {
return null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ReactNode } from 'react'

export default function Layout({
children,
modal,
}: {
children: ReactNode
modal: ReactNode
}) {
return (
<div>
{children}
{modal}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<h1>Example Page</h1>
<Link href="/en/intercepted">Intercept /en/intercepted</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const runtime = 'edge'

export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: Promise<{ locale: string }>
}) {
const locale = (await params).locale
console.log('RootLayout rendered, locale:', locale)

return (
<html lang={locale}>
<body>
<p>Locale: {locale}</p>
{children}
</body>
</html>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Link from 'next/link'

export default function Page() {
return (
<div>
<Link href="/en/example">Go to /en/example</Link>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'

describe('parallel-routes-and-interception-from-root', () => {
const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
})

it('should interpolate [locale] in "/[locale]/example/(...)[locale]/intercepted"', async () => {
const browser = await next.browser('/en/example')

expect(await browser.elementByCss('h1').text()).toBe('Example Page')
expect(await browser.elementByCss('p').text()).toBe('Locale: en')

if (!isNextDeploy) {
expect(next.cliOutput).toInclude('RootLayout rendered, locale: en')
}

const cliOutputLength = next.cliOutput.length

await browser.elementByCss('a').click()

await retry(async () => {
expect(await browser.elementByCss('h2').text()).toBe(
'Page intercepted from root'
)
})

// Ensure that the locale is still correctly rendered in the root layout.
expect(await browser.elementByCss('p').text()).toBe('Locale: en')

// ...and that the root layout was not rerendered.
if (!isNextDeploy) {
expect(next.cliOutput.slice(cliOutputLength)).not.toInclude(
'RootLayout rendered, locale: en'
)
}
})
})

0 comments on commit 4f9a3f4

Please sign in to comment.