diff --git a/packages/next/client/page-loader.js b/packages/next/client/page-loader.js index 9e0b93957da1c0..3c483b9726922a 100644 --- a/packages/next/client/page-loader.js +++ b/packages/next/client/page-loader.js @@ -143,17 +143,21 @@ export default class PageLoader { if ( !Object.keys(dynamicGroups).every((param) => { let value = dynamicMatches[param] - const repeat = dynamicGroups[param].repeat + const { repeat, optional } = dynamicGroups[param] // support single-level catch-all // TODO: more robust handling for user-error (passing `/`) if (repeat && !Array.isArray(value)) value = [value] + let replaced = `[${repeat ? '...' : ''}${param}]` + if (optional) { + replaced = `[${replaced}]` + } return ( param in dynamicMatches && // Interpolate group into data URL if present (interpolatedRoute = interpolatedRoute.replace( - `[${repeat ? '...' : ''}${param}]`, + replaced, repeat ? value.map(encodeURIComponent).join('/') : encodeURIComponent(value) diff --git a/packages/next/next-server/lib/router/utils/route-regex.ts b/packages/next/next-server/lib/router/utils/route-regex.ts index c4502027fac762..5d26a24bb3ee93 100644 --- a/packages/next/next-server/lib/router/utils/route-regex.ts +++ b/packages/next/next-server/lib/router/utils/route-regex.ts @@ -4,6 +4,20 @@ function escapeRegex(str: string) { return str.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&') } +function parseParameter(param: string) { + const optional = /^\\\[.*\\\]$/.test(param) + if (optional) { + param = param.slice(2, -2) + } + const repeat = /^(\\\.){3}/.test(param) + if (repeat) { + param = param.slice(6) + } + // Un-escape key + const key = param.replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1') + return { key, repeat, optional } +} + export function getRouteRegex( normalizedRoute: string ): { @@ -25,21 +39,9 @@ export function getRouteRegex( const parameterizedRoute = escapedRoute.replace( /\/\\\[([^/]+?)\\\](?=\/|$)/g, (_, $1) => { - const isOptional = /^\\\[.*\\\]$/.test($1) - if (isOptional) { - $1 = $1.slice(2, -2) - } - const isCatchAll = /^(\\\.){3}/.test($1) - if (isCatchAll) { - $1 = $1.slice(6) - } - groups[ - $1 - // Un-escape key - .replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1') - // eslint-disable-next-line no-sequences - ] = { pos: groupIndex++, repeat: isCatchAll, optional: isOptional } - return isCatchAll ? (isOptional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)' + const { key, optional, repeat } = parseParameter($1) + groups[key] = { pos: groupIndex++, repeat, optional } + return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)' } ) @@ -53,21 +55,12 @@ export function getRouteRegex( namedParameterizedRoute = escapedRoute.replace( /\/\\\[([^/]+?)\\\](?=\/|$)/g, (_, $1) => { - const isCatchAll = /^(\\\.){3}/.test($1) - const key = $1 - // Un-escape key - .replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1') - .replace(/^\.{3}/, '') - + const { key, repeat } = parseParameter($1) // replace any non-word characters since they can break // the named regex const cleanedKey = key.replace(/\W/g, '') - routeKeys[cleanedKey] = key - - return isCatchAll - ? `/(?<${cleanedKey}>.+?)` - : `/(?<${cleanedKey}>[^/]+?)` + return repeat ? `/(?<${cleanedKey}>.+?)` : `/(?<${cleanedKey}>[^/]+?)` } ) diff --git a/test/integration/prerender/next.config.js b/test/integration/prerender/next.config.js index edfaf6892fe208..28b300dfd147c9 100644 --- a/test/integration/prerender/next.config.js +++ b/test/integration/prerender/next.config.js @@ -1,5 +1,6 @@ module.exports = { experimental: { + optionalCatchAll: true, rewrites() { return [ { diff --git a/test/integration/prerender/pages/catchall-optional/[[...slug]].js b/test/integration/prerender/pages/catchall-optional/[[...slug]].js new file mode 100644 index 00000000000000..2e613944725c99 --- /dev/null +++ b/test/integration/prerender/pages/catchall-optional/[[...slug]].js @@ -0,0 +1,29 @@ +import Link from 'next/link' + +export async function getStaticProps({ params: { slug } }) { + return { + props: { + slug: slug || [], + }, + } +} + +export async function getStaticPaths() { + return { + paths: [{ params: { slug: [] } }, { params: { slug: ['value'] } }], + fallback: false, + } +} + +export default ({ slug }) => { + // Important to not check for `slug` existence (testing that build does not + // render fallback version and error) + return ( + <> +
Catch all: [{slug.join(', ')}]
+ + to home + + > + ) +} diff --git a/test/integration/prerender/pages/index.js b/test/integration/prerender/pages/index.js index 4398d653cc93d6..e826c7c2da83a0 100644 --- a/test/integration/prerender/pages/index.js +++ b/test/integration/prerender/pages/index.js @@ -47,6 +47,13 @@ const Page = ({ world, time }) => { to catchall +