Skip to content

Commit 5f5c5e4

Browse files
ijjkTimer
authored andcommitted
Add support for catch-all routes with SSG (vercel#10175)
* Add support for catchall routes with SSG * Add test for invalid catchall param in getStaticPaths
1 parent 0d0f218 commit 5f5c5e4

File tree

6 files changed

+127
-5
lines changed

6 files changed

+127
-5
lines changed

packages/next/build/utils.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ export async function isPageStatic(
521521
if (hasStaticProps && hasStaticPaths) {
522522
prerenderPaths = [] as string[]
523523

524-
const _routeMatcher = getRouteMatcher(getRouteRegex(page))
524+
const _routeRegex = getRouteRegex(page)
525+
const _routeMatcher = getRouteMatcher(_routeRegex)
525526

526527
// Get the default list of allowed params.
527528
const _validParamKeys = Object.keys(_routeMatcher(page))
@@ -560,15 +561,28 @@ export async function isPageStatic(
560561
const { params = {} } = entry
561562
let builtPage = page
562563
_validParamKeys.forEach(validParamKey => {
563-
if (typeof params[validParamKey] !== 'string') {
564+
const { repeat } = _routeRegex.groups[validParamKey]
565+
const paramValue: string | string[] = params[validParamKey] as
566+
| string
567+
| string[]
568+
if (
569+
(repeat && !Array.isArray(paramValue)) ||
570+
(!repeat && typeof paramValue !== 'string')
571+
) {
564572
throw new Error(
565-
`A required parameter (${validParamKey}) was not provided as a string.`
573+
`A required parameter (${validParamKey}) was not provided as ${
574+
repeat ? 'an array' : 'a string'
575+
}.`
566576
)
567577
}
568578

569579
builtPage = builtPage.replace(
570-
`[${validParamKey}]`,
571-
encodeURIComponent(params[validParamKey])
580+
`[${repeat ? '...' : ''}${validParamKey}]`,
581+
encodeURIComponent(
582+
repeat
583+
? (paramValue as string[]).join('/')
584+
: (paramValue as string)
585+
)
572586
)
573587
})
574588

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import React from 'react'
2+
3+
// eslint-disable-next-line camelcase
4+
export async function unstable_getStaticPaths() {
5+
return [{ params: { slug: 'hello' } }]
6+
}
7+
8+
// eslint-disable-next-line camelcase
9+
export async function unstable_getStaticProps({ params }) {
10+
return {
11+
props: {
12+
post: params.post,
13+
time: (await import('perf_hooks')).performance.now(),
14+
},
15+
}
16+
}
17+
18+
export default () => {
19+
return <div />
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-env jest */
2+
/* global jasmine */
3+
import { join } from 'path'
4+
import { nextBuild } from 'next-test-utils'
5+
6+
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
7+
const appDir = join(__dirname, '..')
8+
9+
describe('Invalid Prerender Catchall Params', () => {
10+
it('should fail the build', async () => {
11+
const out = await nextBuild(appDir, [], { stderr: true })
12+
expect(out.stderr).toMatch(`Build error occurred`)
13+
expect(out.stderr).toMatch(
14+
'A required parameter (slug) was not provided as an array'
15+
)
16+
})
17+
})
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export async function unstable_getStaticProps({ params: { slug } }) {
2+
return {
3+
props: {
4+
slug,
5+
},
6+
revalidate: 1,
7+
}
8+
}
9+
10+
export async function unstable_getStaticPaths() {
11+
return [
12+
{ params: { slug: ['first'] } },
13+
'/catchall/second',
14+
{ params: { slug: ['another', 'value'] } },
15+
'/catchall/hello/another',
16+
]
17+
}
18+
19+
export default ({ slug }) => <p id="catchall">Hi {slug.join('/')}</p>

test/integration/prerender/pages/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ const Page = ({ world, time }) => {
3636
<Link href="/blog/[post]/[comment]" as="/blog/post-1/comment-1">
3737
<a id="comment-1">to another dynamic</a>
3838
</Link>
39+
<Link href="/catchall/[...slug]" as="/catchall/first">
40+
<a id="to-catchall">to catchall</a>
41+
</Link>
3942
</>
4043
)
4144
}

test/integration/prerender/test/index.test.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ const expectedManifestRoutes = () => ({
108108
initialRevalidateSeconds: false,
109109
srcRoute: null,
110110
},
111+
'/catchall/another%2Fvalue': {
112+
dataRoute: `/_next/data/${buildId}/catchall/another%2Fvalue.json`,
113+
initialRevalidateSeconds: 1,
114+
srcRoute: '/catchall/[...slug]',
115+
},
116+
'/catchall/first': {
117+
dataRoute: `/_next/data/${buildId}/catchall/first.json`,
118+
initialRevalidateSeconds: 1,
119+
srcRoute: '/catchall/[...slug]',
120+
},
121+
'/catchall/second': {
122+
dataRoute: `/_next/data/${buildId}/catchall/second.json`,
123+
initialRevalidateSeconds: 1,
124+
srcRoute: '/catchall/[...slug]',
125+
},
126+
'/catchall/hello/another': {
127+
dataRoute: `/_next/data/${buildId}/catchall/hello/another.json`,
128+
initialRevalidateSeconds: 1,
129+
srcRoute: '/catchall/[...slug]',
130+
},
111131
})
112132

113133
const navigateTest = (dev = false) => {
@@ -119,6 +139,7 @@ const navigateTest = (dev = false) => {
119139
'/normal',
120140
'/blog/post-1',
121141
'/blog/post-1/comment-1',
142+
'/catchall/first',
122143
]
123144

124145
await waitFor(2500)
@@ -211,6 +232,15 @@ const navigateTest = (dev = false) => {
211232
expect(text).toMatch(/Comment:.*?comment-1/)
212233
expect(await browser.eval('window.didTransition')).toBe(1)
213234

235+
// go to /catchall/first
236+
await browser.elementByCss('#home').click()
237+
await browser.waitForElementByCss('#to-catchall')
238+
await browser.elementByCss('#to-catchall').click()
239+
await browser.waitForElementByCss('#catchall')
240+
text = await browser.elementByCss('#catchall').text()
241+
expect(text).toMatch(/Hi.*?first/)
242+
expect(await browser.eval('window.didTransition')).toBe(1)
243+
214244
await browser.close()
215245
})
216246
}
@@ -307,6 +337,18 @@ const runTests = (dev = false) => {
307337
expect(await browser.eval('window.beforeClick')).not.toBe('true')
308338
})
309339

340+
it('should support prerendered catchall route', async () => {
341+
const html = await renderViaHTTP(appPort, '/catchall/another/value')
342+
const $ = cheerio.load(html)
343+
expect($('#catchall').text()).toMatch(/Hi.*?another\/value/)
344+
})
345+
346+
it('should support lazy catchall route', async () => {
347+
const html = await renderViaHTTP(appPort, '/catchall/third')
348+
const $ = cheerio.load(html)
349+
expect($('#catchall').text()).toMatch(/Hi.*?third/)
350+
})
351+
310352
if (dev) {
311353
it('should always call getStaticProps without caching in dev', async () => {
312354
const initialRes = await fetchViaHTTP(appPort, '/something')
@@ -414,6 +456,13 @@ const runTests = (dev = false) => {
414456
`^\\/user\\/([^\\/]+?)\\/profile(?:\\/)?$`
415457
),
416458
},
459+
'/catchall/[...slug]': {
460+
routeRegex: normalizeRegEx('^\\/catchall\\/(.+?)(?:\\/)?$'),
461+
dataRoute: `/_next/data/${buildId}/catchall/[...slug].json`,
462+
dataRouteRegex: normalizeRegEx(
463+
`^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\/(.+?)\\.json$`
464+
),
465+
},
417466
})
418467
})
419468

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy