Skip to content

Commit

Permalink
Add support for catch-all routes with SSG (#10175)
Browse files Browse the repository at this point in the history
* Add support for catchall routes with SSG

* Add test for invalid catchall param in getStaticPaths
  • Loading branch information
ijjk authored and Timer committed Jan 20, 2020
1 parent 0d0f218 commit 5f5c5e4
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 5 deletions.
24 changes: 19 additions & 5 deletions packages/next/build/utils.ts
Expand Up @@ -521,7 +521,8 @@ export async function isPageStatic(
if (hasStaticProps && hasStaticPaths) {
prerenderPaths = [] as string[]

const _routeMatcher = getRouteMatcher(getRouteRegex(page))
const _routeRegex = getRouteRegex(page)
const _routeMatcher = getRouteMatcher(_routeRegex)

// Get the default list of allowed params.
const _validParamKeys = Object.keys(_routeMatcher(page))
Expand Down Expand Up @@ -560,15 +561,28 @@ export async function isPageStatic(
const { params = {} } = entry
let builtPage = page
_validParamKeys.forEach(validParamKey => {
if (typeof params[validParamKey] !== 'string') {
const { repeat } = _routeRegex.groups[validParamKey]
const paramValue: string | string[] = params[validParamKey] as
| string
| string[]
if (
(repeat && !Array.isArray(paramValue)) ||
(!repeat && typeof paramValue !== 'string')
) {
throw new Error(
`A required parameter (${validParamKey}) was not provided as a string.`
`A required parameter (${validParamKey}) was not provided as ${
repeat ? 'an array' : 'a string'
}.`
)
}

builtPage = builtPage.replace(
`[${validParamKey}]`,
encodeURIComponent(params[validParamKey])
`[${repeat ? '...' : ''}${validParamKey}]`,
encodeURIComponent(
repeat
? (paramValue as string[]).join('/')
: (paramValue as string)
)
)
})

Expand Down
@@ -0,0 +1,20 @@
import React from 'react'

// eslint-disable-next-line camelcase
export async function unstable_getStaticPaths() {
return [{ params: { slug: 'hello' } }]
}

// eslint-disable-next-line camelcase
export async function unstable_getStaticProps({ params }) {
return {
props: {
post: params.post,
time: (await import('perf_hooks')).performance.now(),
},
}
}

export default () => {
return <div />
}
@@ -0,0 +1,17 @@
/* eslint-env jest */
/* global jasmine */
import { join } from 'path'
import { nextBuild } from 'next-test-utils'

jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2
const appDir = join(__dirname, '..')

describe('Invalid Prerender Catchall Params', () => {
it('should fail the build', async () => {
const out = await nextBuild(appDir, [], { stderr: true })
expect(out.stderr).toMatch(`Build error occurred`)
expect(out.stderr).toMatch(
'A required parameter (slug) was not provided as an array'
)
})
})
19 changes: 19 additions & 0 deletions test/integration/prerender/pages/catchall/[...slug].js
@@ -0,0 +1,19 @@
export async function unstable_getStaticProps({ params: { slug } }) {
return {
props: {
slug,
},
revalidate: 1,
}
}

export async function unstable_getStaticPaths() {
return [
{ params: { slug: ['first'] } },
'/catchall/second',
{ params: { slug: ['another', 'value'] } },
'/catchall/hello/another',
]
}

export default ({ slug }) => <p id="catchall">Hi {slug.join('/')}</p>
3 changes: 3 additions & 0 deletions test/integration/prerender/pages/index.js
Expand Up @@ -36,6 +36,9 @@ const Page = ({ world, time }) => {
<Link href="/blog/[post]/[comment]" as="/blog/post-1/comment-1">
<a id="comment-1">to another dynamic</a>
</Link>
<Link href="/catchall/[...slug]" as="/catchall/first">
<a id="to-catchall">to catchall</a>
</Link>
</>
)
}
Expand Down
49 changes: 49 additions & 0 deletions test/integration/prerender/test/index.test.js
Expand Up @@ -108,6 +108,26 @@ const expectedManifestRoutes = () => ({
initialRevalidateSeconds: false,
srcRoute: null,
},
'/catchall/another%2Fvalue': {
dataRoute: `/_next/data/${buildId}/catchall/another%2Fvalue.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/first': {
dataRoute: `/_next/data/${buildId}/catchall/first.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/second': {
dataRoute: `/_next/data/${buildId}/catchall/second.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
'/catchall/hello/another': {
dataRoute: `/_next/data/${buildId}/catchall/hello/another.json`,
initialRevalidateSeconds: 1,
srcRoute: '/catchall/[...slug]',
},
})

const navigateTest = (dev = false) => {
Expand All @@ -119,6 +139,7 @@ const navigateTest = (dev = false) => {
'/normal',
'/blog/post-1',
'/blog/post-1/comment-1',
'/catchall/first',
]

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

// go to /catchall/first
await browser.elementByCss('#home').click()
await browser.waitForElementByCss('#to-catchall')
await browser.elementByCss('#to-catchall').click()
await browser.waitForElementByCss('#catchall')
text = await browser.elementByCss('#catchall').text()
expect(text).toMatch(/Hi.*?first/)
expect(await browser.eval('window.didTransition')).toBe(1)

await browser.close()
})
}
Expand Down Expand Up @@ -307,6 +337,18 @@ const runTests = (dev = false) => {
expect(await browser.eval('window.beforeClick')).not.toBe('true')
})

it('should support prerendered catchall route', async () => {
const html = await renderViaHTTP(appPort, '/catchall/another/value')
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi.*?another\/value/)
})

it('should support lazy catchall route', async () => {
const html = await renderViaHTTP(appPort, '/catchall/third')
const $ = cheerio.load(html)
expect($('#catchall').text()).toMatch(/Hi.*?third/)
})

if (dev) {
it('should always call getStaticProps without caching in dev', async () => {
const initialRes = await fetchViaHTTP(appPort, '/something')
Expand Down Expand Up @@ -414,6 +456,13 @@ const runTests = (dev = false) => {
`^\\/user\\/([^\\/]+?)\\/profile(?:\\/)?$`
),
},
'/catchall/[...slug]': {
routeRegex: normalizeRegEx('^\\/catchall\\/(.+?)(?:\\/)?$'),
dataRoute: `/_next/data/${buildId}/catchall/[...slug].json`,
dataRouteRegex: normalizeRegEx(
`^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\/(.+?)\\.json$`
),
},
})
})

Expand Down

0 comments on commit 5f5c5e4

Please sign in to comment.