Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions packages/next/build/utils.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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 />
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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