Skip to content

Commit 8bac412

Browse files
authored
make getStaticPaths work with optional catch-all routes (#13559)
Fixes #13524 To do: - [x] fix dev mode - [x] there's a ~route ordering or~ trailing slash issue with top level catch-all (current tests reflect that) - [x] in this test `/get-static-paths/whatever` should fall back to `/[[...optionalName]].js` since `fallback` is `false` in its `getStaticPaths` method. ~Currently seems to 500~ must have been a glitch - [x] add tests for `null`, `undefined` ~and `false`~ behavior as well (if decided these are valid) - [x] ~add tests for string params as well~ this is not allowed for catch-all routes - [x] test behavior when fallback is enabled and a top level catch-all exists
1 parent 9dede10 commit 8bac412

File tree

12 files changed

+314
-34
lines changed

12 files changed

+314
-34
lines changed

packages/next/build/utils.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -636,8 +636,17 @@ export async function buildStaticPaths(
636636
const { params = {} } = entry
637637
let builtPage = page
638638
_validParamKeys.forEach((validParamKey) => {
639-
const { repeat } = _routeRegex.groups[validParamKey]
640-
const paramValue = params[validParamKey]
639+
const { repeat, optional } = _routeRegex.groups[validParamKey]
640+
let paramValue = params[validParamKey]
641+
if (
642+
optional &&
643+
params.hasOwnProperty(validParamKey) &&
644+
(paramValue === null ||
645+
paramValue === undefined ||
646+
(paramValue as any) === false)
647+
) {
648+
paramValue = []
649+
}
641650
if (
642651
(repeat && !Array.isArray(paramValue)) ||
643652
(!repeat && typeof paramValue !== 'string')
@@ -648,13 +657,18 @@ export async function buildStaticPaths(
648657
} in getStaticPaths for ${page}`
649658
)
650659
}
651-
652-
builtPage = builtPage.replace(
653-
`[${repeat ? '...' : ''}${validParamKey}]`,
654-
repeat
655-
? (paramValue as string[]).map(encodeURIComponent).join('/')
656-
: encodeURIComponent(paramValue as string)
657-
)
660+
let replaced = `[${repeat ? '...' : ''}${validParamKey}]`
661+
if (optional) {
662+
replaced = `[${replaced}]`
663+
}
664+
builtPage = builtPage
665+
.replace(
666+
replaced,
667+
repeat
668+
? (paramValue as string[]).map(encodeURIComponent).join('/')
669+
: encodeURIComponent(paramValue as string)
670+
)
671+
.replace(/(?!^)\/$/, '')
658672
})
659673

660674
prerenderPaths?.add(builtPage)

packages/next/export/worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ export default async function exportPage({
348348
} catch (error) {
349349
console.error(
350350
`\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error\n` +
351-
error
351+
error.stack
352352
)
353353
return { ...results, error: true }
354354
}

packages/next/next-server/lib/router/utils/route-regex.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ export function getRouteRegex(
99
): {
1010
re: RegExp
1111
namedRegex?: string
12-
groups: { [groupName: string]: { pos: number; repeat: boolean } }
12+
groups: {
13+
[groupName: string]: { pos: number; repeat: boolean; optional: boolean }
14+
}
1315
} {
1416
// Escape all characters that could be considered RegEx
1517
const escapedRoute = escapeRegex(normalizedRoute.replace(/\/$/, '') || '/')
1618

17-
const groups: { [groupName: string]: { pos: number; repeat: boolean } } = {}
19+
const groups: {
20+
[groupName: string]: { pos: number; repeat: boolean; optional: boolean }
21+
} = {}
1822
let groupIndex = 1
1923

2024
const parameterizedRoute = escapedRoute.replace(
@@ -33,7 +37,7 @@ export function getRouteRegex(
3337
// Un-escape key
3438
.replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1')
3539
// eslint-disable-next-line no-sequences
36-
] = { pos: groupIndex++, repeat: isCatchAll }
40+
] = { pos: groupIndex++, repeat: isCatchAll, optional: isOptional }
3741
return isCatchAll ? (isOptional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'
3842
}
3943
)

packages/next/next-server/server/next-server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,9 @@ export default class Server {
983983
? (req as any)._nextRewroteUrl
984984
: `${parseUrl(req.url || '').pathname!}`
985985

986+
// remove trailing slash
987+
urlPathname = urlPathname.replace(/(?!^)\/$/, '')
988+
986989
// remove /_next/data prefix from urlPathname so it matches
987990
// for direct page visit and /_next/data visit
988991
if (isDataReq && urlPathname.includes(this.buildId)) {

test/integration/dynamic-optional-routing/pages/[[...optionalName]].js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ export async function getServerSideProps({ query }) {
88

99
export default function Page(props) {
1010
return (
11-
<div id="optional-route">
11+
<div id="route">
1212
top level route param:{' '}
1313
{props.query.optionalName === undefined
1414
? 'undefined'
15-
: `[${props.query.optionalName.join(',')}]`}
15+
: `[${props.query.optionalName.join('|')}]`}
1616
</div>
1717
)
1818
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useRouter } from 'next/router'
2+
3+
export async function getStaticPaths() {
4+
return {
5+
paths: [
6+
{
7+
params: { slug: [] },
8+
},
9+
{
10+
params: { slug: ['p1'] },
11+
},
12+
{
13+
params: { slug: ['p2', 'p3'] },
14+
},
15+
],
16+
fallback: true,
17+
}
18+
}
19+
20+
export async function getStaticProps({ params }) {
21+
return { props: { params } }
22+
}
23+
24+
export default function Index(props) {
25+
const router = useRouter()
26+
return (
27+
<div id="route">
28+
gsp fallback route:{' '}
29+
{props.params?.slug === undefined
30+
? 'undefined'
31+
: `[${props.params.slug.join('|')}]`}
32+
{router.isFallback ? ' is fallback' : ' is not fallback'}
33+
</div>
34+
)
35+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export async function getStaticPaths() {
2+
return {
3+
paths: [
4+
{
5+
params: { slug: false },
6+
},
7+
],
8+
fallback: false,
9+
}
10+
}
11+
12+
export async function getStaticProps({ params }) {
13+
return { props: { params } }
14+
}
15+
16+
export default function Index(props) {
17+
return (
18+
<div id="route">
19+
gsp false route:{' '}
20+
{props.params.slug === undefined
21+
? 'undefined'
22+
: `[${props.params.slug.join('|')}]`}
23+
</div>
24+
)
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export async function getStaticPaths() {
2+
return {
3+
paths: [
4+
{
5+
params: { slug: null },
6+
},
7+
],
8+
fallback: false,
9+
}
10+
}
11+
12+
export async function getStaticProps({ params }) {
13+
return { props: { params } }
14+
}
15+
16+
export default function Index(props) {
17+
return (
18+
<div id="route">
19+
gsp null route:{' '}
20+
{props.params.slug === undefined
21+
? 'undefined'
22+
: `[${props.params.slug.join('|')}]`}
23+
</div>
24+
)
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export async function getStaticPaths() {
2+
return {
3+
paths: [
4+
{
5+
params: { slug: undefined },
6+
},
7+
],
8+
fallback: false,
9+
}
10+
}
11+
12+
export async function getStaticProps({ params }) {
13+
return { props: { params } }
14+
}
15+
16+
export default function Index(props) {
17+
return (
18+
<div id="route">
19+
gsp undefined route:{' '}
20+
{props.params.slug === undefined
21+
? 'undefined'
22+
: `[${props.params.slug.join('|')}]`}
23+
</div>
24+
)
25+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export async function getStaticPaths() {
2+
return {
3+
paths: [
4+
{
5+
params: { slug: [] },
6+
},
7+
{
8+
params: { slug: ['p1'] },
9+
},
10+
{
11+
params: { slug: ['p2', 'p3'] },
12+
},
13+
],
14+
fallback: false,
15+
}
16+
}
17+
18+
export async function getStaticProps({ params }) {
19+
return { props: { params } }
20+
}
21+
22+
export default function Index(props) {
23+
return (
24+
<div id="route">
25+
gsp route:{' '}
26+
{props.params.slug === undefined
27+
? 'undefined'
28+
: `[${props.params.slug.join('|')}]`}
29+
</div>
30+
)
31+
}

0 commit comments

Comments
 (0)