diff --git a/errors/invalid-getstaticpaths-value.md b/errors/invalid-getstaticpaths-value.md index 49ba006415..47dfe60a7b 100644 --- a/errors/invalid-getstaticpaths-value.md +++ b/errors/invalid-getstaticpaths-value.md @@ -11,7 +11,30 @@ Make sure to return the following shape from `unstable_getStaticPaths`: ```js export async function unstable_getStaticPaths() { return { - paths: Array + paths: Array, + fallback: boolean } } ``` + +There are two required properties: + +1. `paths`: this property is an [Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) of URLs ("paths") that should be statically generated at build-time. The returned paths must match the dynamic route shape. + - You may return a [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) or an [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that explicitly defines all URL `params`. + ```js + // pages/blog/[slug].js + export async function unstable_getStaticPaths() { + return { + paths: [ + // String variant: + '/blog/first-post', + // Object variant: + { params: { slug: 'second-post' } }, + ], + fallback: true, + } + } + ``` +1. `fallback`: this property is a [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean), specifying whether or not a fallback version of this page should be generated. + - Enabling `fallback` (via `true`) allows you to return a subset of all the possible paths that should be statically generated. At runtime, Next.js will statically generate the remaining paths the **first time they are requested**. Consecutive calls to the path will be served as-if it was statically generated at build-time. This reduces build times when dealing with thousands or millions of pages. + - Disabling `fallback` (via `false`) requires you return the full collection of paths you would like to statically generate at build-time. At runtime, any path that was not generated at build-time **will 404**. diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 849dff8b95..1ce20330f1 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -90,13 +90,13 @@ export type SsgRoute = { export type DynamicSsgRoute = { routeRegex: string - fallback: string + fallback: string | false dataRoute: string dataRouteRegex: string } export type PrerenderManifest = { - version: number + version: 2 routes: { [route: string]: SsgRoute } dynamicRoutes: { [route: string]: DynamicSsgRoute } preview: __ApiPreviewProps @@ -432,6 +432,7 @@ export default async function build(dir: string, conf = null): Promise { const buildManifestPath = path.join(distDir, BUILD_MANIFEST) const ssgPages = new Set() + const ssgFallbackPages = new Set() const staticPages = new Set() const invalidPages = new Set() const hybridAmpPages = new Set() @@ -478,6 +479,7 @@ export default async function build(dir: string, conf = null): Promise { let isStatic = false let isHybridAmp = false let ssgPageRoutes: string[] | null = null + let hasSsgFallback: boolean = false pagesManifest[page] = bundleRelative.replace(/\\/g, '/') @@ -533,6 +535,10 @@ export default async function build(dir: string, conf = null): Promise { additionalSsgPaths.set(page, result.prerenderRoutes) ssgPageRoutes = result.prerenderRoutes } + if (result.prerenderFallback) { + hasSsgFallback = true + ssgFallbackPages.add(page) + } } else if (result.hasServerProps) { serverPropsPages.add(page) } else if (result.isStatic && customAppGetInitialProps === false) { @@ -564,6 +570,7 @@ export default async function build(dir: string, conf = null): Promise { isSsg, isHybridAmp, ssgPageRoutes, + hasSsgFallback, }) }) ) @@ -669,9 +676,15 @@ export default async function build(dir: string, conf = null): Promise { if (isDynamicRoute(page)) { tbdPrerenderRoutes.push(page) - // Override the rendering for the dynamic page to be treated as a - // fallback render. - defaultMap[page] = { page, query: { __nextFallback: true } } + if (ssgFallbackPages.has(page)) { + // Override the rendering for the dynamic page to be treated as a + // fallback render. + defaultMap[page] = { page, query: { __nextFallback: true } } + } else { + // Remove dynamically routed pages from the default path map when + // fallback behavior is disabled. + delete defaultMap[page] + } } }) // Append the "well-known" routes we should prerender for, e.g. blog @@ -736,12 +749,16 @@ export default async function build(dir: string, conf = null): Promise { for (const page of combinedPages) { const isSsg = ssgPages.has(page) + const isSsgFallback = ssgFallbackPages.has(page) const isDynamic = isDynamicRoute(page) const hasAmp = hybridAmpPages.has(page) let file = normalizePagePath(page) - // We should always have an HTML file to move for each page - await moveExportedPage(page, file, isSsg, 'html') + // The dynamic version of SSG pages are only prerendered if the fallback + // is enabled. Below, we handle the specific prerenders of these. + if (!(isSsg && isDynamic && !isSsgFallback)) { + await moveExportedPage(page, file, isSsg, 'html') + } if (hasAmp) { await moveExportedPage(`${page}.amp`, `${file}.amp`, isSsg, 'html') @@ -760,7 +777,7 @@ export default async function build(dir: string, conf = null): Promise { } } else { // For a dynamic SSG page, we did not copy its data exports and only - // copy the fallback HTML file. + // copy the fallback HTML file (if present). // We must also copy specific versions of this page as defined by // `unstable_getStaticPaths` (additionalSsgPaths). const extraRoutes = additionalSsgPaths.get(page) || [] @@ -814,14 +831,16 @@ export default async function build(dir: string, conf = null): Promise { finalDynamicRoutes[tbdRoute] = { routeRegex: getRouteRegex(tbdRoute).re.source, dataRoute, - fallback: `${normalizedRoute}.html`, + fallback: ssgFallbackPages.has(tbdRoute) + ? `${normalizedRoute}.html` + : false, dataRouteRegex: getRouteRegex( dataRoute.replace(/\.json$/, '') ).re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$'), } }) const prerenderManifest: PrerenderManifest = { - version: 1, + version: 2, routes: finalPrerenderRoutes, dynamicRoutes: finalDynamicRoutes, preview: previewProps, @@ -834,7 +853,7 @@ export default async function build(dir: string, conf = null): Promise { ) } else { const prerenderManifest: PrerenderManifest = { - version: 1, + version: 2, routes: {}, dynamicRoutes: {}, preview: previewProps, diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 8b9d19cb69..65e563c2ae 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -42,6 +42,7 @@ export interface PageInfo { static: boolean isSsg: boolean ssgPageRoutes: string[] | null + hasSsgFallback: boolean serverBundle: string } @@ -500,7 +501,7 @@ export async function getPageSizeInKb( export async function buildStaticPaths( page: string, unstable_getStaticPaths: Unstable_getStaticPaths -): Promise> { +): Promise<{ paths: string[]; fallback: boolean }> { const prerenderPaths = new Set() const _routeRegex = getRouteRegex(page) const _routeMatcher = getRouteMatcher(_routeRegex) @@ -511,7 +512,7 @@ export async function buildStaticPaths( const staticPathsResult = await unstable_getStaticPaths() const expectedReturnVal = - `Expected: { paths: [] }\n` + + `Expected: { paths: [], fallback: boolean }\n` + `See here for more info: https://err.sh/zeit/next.js/invalid-getstaticpaths-value` if ( @@ -525,7 +526,7 @@ export async function buildStaticPaths( } const invalidStaticPathKeys = Object.keys(staticPathsResult).filter( - key => key !== 'paths' + key => !(key === 'paths' || key === 'fallback') ) if (invalidStaticPathKeys.length > 0) { @@ -536,6 +537,13 @@ export async function buildStaticPaths( ) } + if (typeof staticPathsResult.fallback !== 'boolean') { + throw new Error( + `The \`fallback\` key must be returned from unstable_getStaticProps in ${page}.\n` + + expectedReturnVal + ) + } + const toPrerender = staticPathsResult.paths if (!Array.isArray(toPrerender)) { @@ -601,7 +609,7 @@ export async function buildStaticPaths( } }) - return [...prerenderPaths] + return { paths: [...prerenderPaths], fallback: staticPathsResult.fallback } } export async function isPageStatic( @@ -614,6 +622,7 @@ export async function isPageStatic( hasServerProps?: boolean hasStaticProps?: boolean prerenderRoutes?: string[] | undefined + prerenderFallback?: boolean | undefined }> { try { require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig) @@ -667,11 +676,12 @@ export async function isPageStatic( } let prerenderRoutes: Array | undefined + let prerenderFallback: boolean | undefined if (hasStaticProps && hasStaticPaths) { - prerenderRoutes = await buildStaticPaths( - page, - mod.unstable_getStaticPaths - ) + ;({ + paths: prerenderRoutes, + fallback: prerenderFallback, + } = await buildStaticPaths(page, mod.unstable_getStaticPaths)) } const config = mod.config || {} @@ -679,6 +689,7 @@ export async function isPageStatic( isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps, isHybridAmp: config.amp === 'hybrid', prerenderRoutes, + prerenderFallback, hasStaticProps, hasServerProps, } diff --git a/packages/next/export/worker.js b/packages/next/export/worker.js index 3997d954e3..ae5b4bd0b0 100644 --- a/packages/next/export/worker.js +++ b/packages/next/export/worker.js @@ -264,7 +264,7 @@ export default async function({ return results } catch (error) { console.error( - `\nError occurred prerendering page "${path}" https://err.sh/next.js/prerender-error:\n` + + `\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error:\n` + error ) return { ...results, error: true } diff --git a/packages/next/next-server/server/load-components.ts b/packages/next/next-server/server/load-components.ts index b088480e48..e4052770e8 100644 --- a/packages/next/next-server/server/load-components.ts +++ b/packages/next/next-server/server/load-components.ts @@ -52,6 +52,7 @@ type Unstable_getStaticProps = (ctx: { export type Unstable_getStaticPaths = () => Promise<{ paths: Array + fallback: boolean }> type Unstable_getServerProps = (context: { diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index e4fbcc676c..c02696bcf7 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -6,6 +6,7 @@ import nanoid from 'next/dist/compiled/nanoid/index.js' import { join, resolve, sep } from 'path' import { parse as parseQs, ParsedUrlQuery } from 'querystring' import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url' +import { PrerenderManifest } from '../../build' import { getRedirectStatus, Header, @@ -292,15 +293,17 @@ export default class Server { return require(join(this.distDir, ROUTES_MANIFEST)) } - private _cachedPreviewProps: __ApiPreviewProps | undefined - protected getPreviewProps(): __ApiPreviewProps { - if (this._cachedPreviewProps) { - return this._cachedPreviewProps + private _cachedPreviewManifest: PrerenderManifest | undefined + protected getPrerenderManifest(): PrerenderManifest { + if (this._cachedPreviewManifest) { + return this._cachedPreviewManifest } - return (this._cachedPreviewProps = require(join( - this.distDir, - PRERENDER_MANIFEST - )).preview) + const manifest = require(join(this.distDir, PRERENDER_MANIFEST)) + return (this._cachedPreviewManifest = manifest) + } + + protected getPreviewProps(): __ApiPreviewProps { + return this.getPrerenderManifest().preview } protected generateRoutes(): { @@ -870,7 +873,7 @@ export default class Server { pathname: string, { components, query }: FindComponentsResult, opts: any - ): Promise { + ): Promise { // we need to ensure the status code if /404 is visited directly if (pathname === '/404') { res.statusCode = 404 @@ -1029,24 +1032,35 @@ export default class Server { // we lazy load the staticPaths to prevent the user // from waiting on them for the page to load in dev mode let staticPaths: string[] | undefined + let hasStaticFallback = false - if (!isProduction && hasStaticPaths) { - const __getStaticPaths = async () => { - const paths = await this.staticPathsWorker!.loadStaticPaths( - this.distDir, - this.buildId, - pathname, - !this.renderOpts.dev && this._isLikeServerless - ) - return paths + if (hasStaticPaths) { + if (isProduction) { + // `staticPaths` is intentionally set to `undefined` as it should've + // been caught above when checking disk data. + staticPaths = undefined + + // Read whether or not fallback should exist from the manifest. + hasStaticFallback = + typeof this.getPrerenderManifest().dynamicRoutes[pathname] + .fallback === 'string' + } else { + const __getStaticPaths = async () => { + const paths = await this.staticPathsWorker!.loadStaticPaths( + this.distDir, + this.buildId, + pathname, + !this.renderOpts.dev && this._isLikeServerless + ) + return paths + } + ;({ paths: staticPaths, fallback: hasStaticFallback } = ( + await withCoalescedInvoke(__getStaticPaths)( + `staticPaths-${pathname}`, + [] + ) + ).value) } - - staticPaths = ( - await withCoalescedInvoke(__getStaticPaths)( - `staticPaths-${pathname}`, - [] - ) - ).value } // const isForcedBlocking = @@ -1074,6 +1088,16 @@ export default class Server { // `getStaticPaths` (isProduction || !staticPaths || !staticPaths.includes(urlPathname)) ) { + if ( + // In development, fall through to render to handle missing + // getStaticPaths. + (isProduction || staticPaths) && + // When fallback isn't present, abort this render so we 404 + !hasStaticFallback + ) { + return false + } + let html: string // Production already emitted the fallback as static HTML. @@ -1137,13 +1161,16 @@ export default class Server { try { const result = await this.findPageComponents(pathname, query) if (result) { - return await this.renderToHTMLWithComponents( + const result2 = await this.renderToHTMLWithComponents( req, res, pathname, result, { ...this.renderOpts, amphtml, hasAmp } ) + if (result2 !== false) { + return result2 + } } if (this.dynamicRoutes) { @@ -1159,7 +1186,7 @@ export default class Server { params ) if (result) { - return await this.renderToHTMLWithComponents( + const result2 = await this.renderToHTMLWithComponents( req, res, dynamicRoute.page, @@ -1171,6 +1198,9 @@ export default class Server { hasAmp, } ) + if (result2 !== false) { + return result2 + } } } } @@ -1224,9 +1254,9 @@ export default class Server { result = await this.findPageComponents('/_error', query) } - let html + let html: string | null try { - html = await this.renderToHTMLWithComponents( + const result2 = await this.renderToHTMLWithComponents( req, res, using404Page ? '/404' : '/_error', @@ -1236,6 +1266,10 @@ export default class Server { err, } ) + if (result2 === false) { + throw new Error('invariant: failed to render error page') + } + html = result2 } catch (err) { console.error(err) res.statusCode = 500 diff --git a/packages/next/next-server/server/spr-cache.ts b/packages/next/next-server/server/spr-cache.ts index 072496d9c0..c449fcd331 100644 --- a/packages/next/next-server/server/spr-cache.ts +++ b/packages/next/next-server/server/spr-cache.ts @@ -74,7 +74,7 @@ export function initializeSprCache({ if (dev) { prerenderManifest = { - version: -1, + version: -1 as any, // letting us know this doesn't conform to spec routes: {}, dynamicRoutes: {}, preview: null as any, // `preview` is special case read in next-dev-server diff --git a/test/integration/catches-missing-getStaticProps/pages/[slug].js b/test/integration/catches-missing-getStaticProps/pages/[slug].js index 9c46680135..7a87525aac 100644 --- a/test/integration/catches-missing-getStaticProps/pages/[slug].js +++ b/test/integration/catches-missing-getStaticProps/pages/[slug].js @@ -1,5 +1,5 @@ export async function unstable_getStaticPaths() { - return { paths: ['/hello', '/world'] } + return { paths: ['/hello', '/world'], fallback: true } } export default () =>

something is missing 🤔

diff --git a/test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js b/test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js index 0d054a972a..85ace5dad1 100644 --- a/test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js +++ b/test/integration/dynamic-routing/pages/p1/p2/all-ssg/[...rest].js @@ -9,6 +9,7 @@ export function unstable_getStaticProps({ params }) { export function unstable_getStaticPaths() { return { paths: [], + fallback: true, } } diff --git a/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js b/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js index cb57d820b3..03418a2de5 100644 --- a/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js +++ b/test/integration/dynamic-routing/pages/p1/p2/nested-all-ssg/[...rest]/index.js @@ -15,6 +15,7 @@ export function unstable_getStaticProps({ params }) { export function unstable_getStaticPaths() { return { paths: [], + fallback: true, } } diff --git a/test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js b/test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js index 72abcefb08..55cf9c8a29 100644 --- a/test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js +++ b/test/integration/dynamic-routing/pages/p1/p2/predefined-ssg/[...rest].js @@ -12,6 +12,7 @@ export function unstable_getStaticPaths() { `/p1/p2/predefined-ssg/one-level`, `/p1/p2/predefined-ssg/1st-level/2nd-level`, ], + fallback: true, } } diff --git a/test/integration/mixed-ssg-serverprops-error/pages/index.js.alt b/test/integration/mixed-ssg-serverprops-error/pages/index.js.alt index 910eef2206..4756984cc3 100644 --- a/test/integration/mixed-ssg-serverprops-error/pages/index.js.alt +++ b/test/integration/mixed-ssg-serverprops-error/pages/index.js.alt @@ -1,6 +1,6 @@ export const unstable_getStaticPaths = async () => { return { - props: { world: 'world' } + props: { world: 'world' }, fallback: true } } diff --git a/test/integration/prerender-invalid-catchall-params/pages/[...slug].js b/test/integration/prerender-invalid-catchall-params/pages/[...slug].js index 8ef33a1937..f8413c5dc5 100644 --- a/test/integration/prerender-invalid-catchall-params/pages/[...slug].js +++ b/test/integration/prerender-invalid-catchall-params/pages/[...slug].js @@ -2,7 +2,7 @@ import React from 'react' // eslint-disable-next-line camelcase export async function unstable_getStaticPaths() { - return { paths: [{ params: { slug: 'hello' } }] } + return { paths: [{ params: { slug: 'hello' } }], fallback: true } } // eslint-disable-next-line camelcase diff --git a/test/integration/prerender-invalid-paths/pages/[foo]/[post].js b/test/integration/prerender-invalid-paths/pages/[foo]/[post].js index 4e0933f554..ef80ed41e8 100644 --- a/test/integration/prerender-invalid-paths/pages/[foo]/[post].js +++ b/test/integration/prerender-invalid-paths/pages/[foo]/[post].js @@ -2,7 +2,7 @@ import React from 'react' // eslint-disable-next-line camelcase export async function unstable_getStaticPaths() { - return { paths: [{ foo: 'bad', baz: 'herro' }] } + return { paths: [{ foo: 'bad', baz: 'herro' }], fallback: true } } // eslint-disable-next-line camelcase diff --git a/test/integration/prerender/pages/blog/[post]/[comment].js b/test/integration/prerender/pages/blog/[post]/[comment].js index a64b89b540..0c1174ffbb 100644 --- a/test/integration/prerender/pages/blog/[post]/[comment].js +++ b/test/integration/prerender/pages/blog/[post]/[comment].js @@ -8,6 +8,7 @@ export async function unstable_getStaticPaths() { '/blog/post-1/comment-1', { params: { post: 'post-2', comment: 'comment-2' } }, ], + fallback: true, } } diff --git a/test/integration/prerender/pages/blog/[post]/index.js b/test/integration/prerender/pages/blog/[post]/index.js index e96ceaf1a2..2b63657aae 100644 --- a/test/integration/prerender/pages/blog/[post]/index.js +++ b/test/integration/prerender/pages/blog/[post]/index.js @@ -13,6 +13,7 @@ export async function unstable_getStaticPaths() { '/blog/post.1', '/blog/post.1', // handle duplicates ], + fallback: true, } } diff --git a/test/integration/prerender/pages/catchall-explicit/[...slug].js b/test/integration/prerender/pages/catchall-explicit/[...slug].js new file mode 100644 index 0000000000..e992e8c79b --- /dev/null +++ b/test/integration/prerender/pages/catchall-explicit/[...slug].js @@ -0,0 +1,30 @@ +export async function unstable_getStaticProps({ params: { slug } }) { + if (slug[0] === 'delayby3s') { + await new Promise(resolve => setTimeout(resolve, 3000)) + } + + return { + props: { + slug, + }, + revalidate: 1, + } +} + +export async function unstable_getStaticPaths() { + return { + paths: [ + { params: { slug: ['first'] } }, + '/catchall-explicit/second', + { params: { slug: ['another', 'value'] } }, + '/catchall-explicit/hello/another', + ], + fallback: false, + } +} + +export default ({ slug }) => { + // Important to not check for `slug` existence (testing that build does not + // render fallback version and error) + return

Hi {slug.join(' ')}

+} diff --git a/test/integration/prerender/pages/catchall/[...slug].js b/test/integration/prerender/pages/catchall/[...slug].js index b0010c6124..5a4834499e 100644 --- a/test/integration/prerender/pages/catchall/[...slug].js +++ b/test/integration/prerender/pages/catchall/[...slug].js @@ -21,6 +21,7 @@ export async function unstable_getStaticPaths() { { params: { slug: ['another', 'value'] } }, '/catchall/hello/another', ], + fallback: true, } } diff --git a/test/integration/prerender/pages/user/[user]/profile.js b/test/integration/prerender/pages/user/[user]/profile.js index 47f3bcd3cd..a80e175b6d 100644 --- a/test/integration/prerender/pages/user/[user]/profile.js +++ b/test/integration/prerender/pages/user/[user]/profile.js @@ -3,7 +3,7 @@ import Link from 'next/link' // eslint-disable-next-line camelcase export async function unstable_getStaticPaths() { - return { paths: [] } + return { paths: [], fallback: true } } // eslint-disable-next-line camelcase diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 7d52eaed28..823437388d 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -92,6 +92,26 @@ const expectedManifestRoutes = () => ({ initialRevalidateSeconds: 10, srcRoute: '/blog/[post]', }, + '/catchall-explicit/another/value': { + dataRoute: `/_next/data/${buildId}/catchall-explicit/another/value.json`, + initialRevalidateSeconds: 1, + srcRoute: '/catchall-explicit/[...slug]', + }, + '/catchall-explicit/first': { + dataRoute: `/_next/data/${buildId}/catchall-explicit/first.json`, + initialRevalidateSeconds: 1, + srcRoute: '/catchall-explicit/[...slug]', + }, + '/catchall-explicit/hello/another': { + dataRoute: `/_next/data/${buildId}/catchall-explicit/hello/another.json`, + initialRevalidateSeconds: 1, + srcRoute: '/catchall-explicit/[...slug]', + }, + '/catchall-explicit/second': { + dataRoute: `/_next/data/${buildId}/catchall-explicit/second.json`, + initialRevalidateSeconds: 1, + srcRoute: '/catchall-explicit/[...slug]', + }, '/another': { dataRoute: `/_next/data/${buildId}/another.json`, initialRevalidateSeconds: 1, @@ -418,6 +438,49 @@ const runTests = (dev = false, looseMode = false) => { ) }) + it('should support prerendered catchall-explicit route (nested)', async () => { + const html = await renderViaHTTP( + appPort, + '/catchall-explicit/another/value' + ) + const $ = cheerio.load(html) + + expect( + JSON.parse( + cheerio + .load(html)('#__NEXT_DATA__') + .text() + ).isFallback + ).toBe(false) + expect($('#catchall').text()).toMatch(/Hi.*?another value/) + }) + + it('should support prerendered catchall-explicit route (single)', async () => { + const html = await renderViaHTTP(appPort, '/catchall-explicit/second') + const $ = cheerio.load(html) + + expect( + JSON.parse( + cheerio + .load(html)('#__NEXT_DATA__') + .text() + ).isFallback + ).toBe(false) + expect($('#catchall').text()).toMatch(/Hi.*?second/) + }) + + if (!looseMode) { + it('should 404 for a missing catchall explicit route', async () => { + const res = await fetchViaHTTP( + appPort, + '/catchall-explicit/notreturnedinpaths' + ) + expect(res.status).toBe(404) + const html = await res.text() + expect(html).toMatch(/This page could not be found/) + }) + } + if (dev) { // TODO: re-enable when this is supported in dev // it('should show error when rewriting to dynamic SSG page', async () => { @@ -561,6 +624,36 @@ const runTests = (dev = false, looseMode = false) => { } }) + it('should error on dynamic page without getStaticPaths returning fallback property', async () => { + const curPage = join(__dirname, '../pages/temp2/[slug].js') + await fs.mkdirp(dirname(curPage)) + await fs.writeFile( + curPage, + ` + export async function unstable_getStaticPaths() { + return { + paths: [] + } + } + export async function unstable_getStaticProps() { + return { + props: { + hello: 'world' + } + } + } + export default () => 'oops' + ` + ) + await waitFor(1000) + try { + const html = await renderViaHTTP(appPort, '/temp2/hello') + expect(html).toMatch(/`fallback` key must be returned from/) + } finally { + await fs.remove(curPage) + } + }) + it('should not re-call getStaticProps when updating query', async () => { const browser = await webdriver(appPort, '/something?hello=world') await waitFor(2000) @@ -641,6 +734,14 @@ const runTests = (dev = false, looseMode = false) => { ), page: '/catchall/[...slug]', }, + { + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/catchall\\-explicit\\/(.+?)\\.json$` + ), + page: '/catchall-explicit/[...slug]', + }, { dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapeRegex( @@ -683,7 +784,7 @@ const runTests = (dev = false, looseMode = false) => { } }) - expect(manifest.version).toBe(1) + expect(manifest.version).toBe(2) expect(manifest.routes).toEqual(expectedManifestRoutes()) expect(manifest.dynamicRoutes).toEqual({ '/blog/[post]': { @@ -722,6 +823,16 @@ const runTests = (dev = false, looseMode = false) => { `^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\/(.+?)\\.json$` ), }, + '/catchall-explicit/[...slug]': { + dataRoute: `/_next/data/${buildId}/catchall-explicit/[...slug].json`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapedBuildId}\\/catchall\\-explicit\\/(.+?)\\.json$` + ), + fallback: false, + routeRegex: normalizeRegEx( + '^\\/catchall\\-explicit\\/(.+?)(?:\\/)?$' + ), + }, }) })