Ability to Disable SSG Fallback (#10701)
* Ability to Disable SSG Fallback * Throw error when value is missing * Fix existing tests * Adjust error message * Do not render fallback at build time for `fallback: false` page * Fix existing fallback behavior * fix build * fix version * fix some tests * Fix last test * Add docs for get static paths * Add explicit mode tests * test for fallback error message
This commit is contained in:
parent
b3ffdabbad
commit
47ff1eb95a
20 changed files with 292 additions and 57 deletions
|
@ -11,7 +11,30 @@ Make sure to return the following shape from `unstable_getStaticPaths`:
|
|||
```js
|
||||
export async function unstable_getStaticPaths() {
|
||||
return {
|
||||
paths: Array<string | { params: { [key: string]: string } }>
|
||||
paths: Array<string | { params: { [key: string]: string } }>,
|
||||
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**.
|
||||
|
|
|
@ -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<void> {
|
|||
const buildManifestPath = path.join(distDir, BUILD_MANIFEST)
|
||||
|
||||
const ssgPages = new Set<string>()
|
||||
const ssgFallbackPages = new Set<string>()
|
||||
const staticPages = new Set<string>()
|
||||
const invalidPages = new Set<string>()
|
||||
const hybridAmpPages = new Set<string>()
|
||||
|
@ -478,6 +479,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
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<void> {
|
|||
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<void> {
|
|||
isSsg,
|
||||
isHybridAmp,
|
||||
ssgPageRoutes,
|
||||
hasSsgFallback,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
@ -669,9 +676,15 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
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<void> {
|
|||
|
||||
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<void> {
|
|||
}
|
||||
} 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<void> {
|
|||
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<void> {
|
|||
)
|
||||
} else {
|
||||
const prerenderManifest: PrerenderManifest = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
routes: {},
|
||||
dynamicRoutes: {},
|
||||
preview: previewProps,
|
||||
|
|
|
@ -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<Array<string>> {
|
||||
): Promise<{ paths: string[]; fallback: boolean }> {
|
||||
const prerenderPaths = new Set<string>()
|
||||
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<string> | 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,
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -52,6 +52,7 @@ type Unstable_getStaticProps = (ctx: {
|
|||
|
||||
export type Unstable_getStaticPaths = () => Promise<{
|
||||
paths: Array<string | { params: ParsedUrlQuery }>
|
||||
fallback: boolean
|
||||
}>
|
||||
|
||||
type Unstable_getServerProps = (context: {
|
||||
|
|
|
@ -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<string | null> {
|
||||
): Promise<string | false | null> {
|
||||
// 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export async function unstable_getStaticPaths() {
|
||||
return { paths: ['/hello', '/world'] }
|
||||
return { paths: ['/hello', '/world'], fallback: true }
|
||||
}
|
||||
|
||||
export default () => <p>something is missing 🤔</p>
|
||||
|
|
|
@ -9,6 +9,7 @@ export function unstable_getStaticProps({ params }) {
|
|||
export function unstable_getStaticPaths() {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ export function unstable_getStaticProps({ params }) {
|
|||
export function unstable_getStaticPaths() {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ export function unstable_getStaticPaths() {
|
|||
`/p1/p2/predefined-ssg/one-level`,
|
||||
`/p1/p2/predefined-ssg/1st-level/2nd-level`,
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const unstable_getStaticPaths = async () => {
|
||||
return {
|
||||
props: { world: 'world' }
|
||||
props: { world: 'world' }, fallback: true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,6 +8,7 @@ export async function unstable_getStaticPaths() {
|
|||
'/blog/post-1/comment-1',
|
||||
{ params: { post: 'post-2', comment: 'comment-2' } },
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ export async function unstable_getStaticPaths() {
|
|||
'/blog/post.1',
|
||||
'/blog/post.1', // handle duplicates
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 <p id="catchall">Hi {slug.join(' ')}</p>
|
||||
}
|
|
@ -21,6 +21,7 @@ export async function unstable_getStaticPaths() {
|
|||
{ params: { slug: ['another', 'value'] } },
|
||||
'/catchall/hello/another',
|
||||
],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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\\/(.+?)(?:\\/)?$'
|
||||
),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue