Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for catch-all routes with SSG #10175

Merged
merged 6 commits into from Jan 20, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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