From 7bf0454203721af5d0a5a180302a5b46bb92101b Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Feb 2020 17:18:41 -0600 Subject: [PATCH 01/17] Add initial SSG fallback handling --- packages/next/build/index.ts | 48 ++++++------ packages/next/build/ssg.ts | 76 +++++++++++++++++++ packages/next/build/utils.ts | 74 ++---------------- packages/next/client/index.js | 6 ++ .../next/next-server/lib/router/router.ts | 76 +++++++++++++------ packages/next/next-server/lib/utils.ts | 1 + .../next-server/server/load-components.ts | 4 +- .../next/next-server/server/next-server.ts | 34 +++++++++ packages/next/next-server/server/render.tsx | 17 ++++- .../prerender/pages/blog/[post]/[comment].js | 7 ++ .../prerender/pages/catchall/[...slug].js | 2 +- yarn.lock | 27 +------ 12 files changed, 228 insertions(+), 144 deletions(-) create mode 100644 packages/next/build/ssg.ts diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 4d4307882bf0c..0a988502097b3 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -67,6 +67,7 @@ import { } from './utils' import getBaseWebpackConfig from './webpack-config' import { writeBuildId } from './write-build-id' +import { normalizePagePath } from '../next-server/server/normalize-page-path' const fsAccess = promisify(fs.access) const fsUnlink = promisify(fs.unlink) @@ -87,7 +88,7 @@ export type SsgRoute = { export type DynamicSsgRoute = { routeRegex: string - + fallback: string dataRoute: string dataRouteRegex: string } @@ -435,7 +436,7 @@ export default async function build(dir: string, conf = null): Promise { const analysisBegin = process.hrtime() await Promise.all( pageKeys.map(async page => { - const actualPage = page === '/' ? '/index' : page + const actualPage = normalizePagePath(page) const [selfSize, allSize] = await getPageSizeInKb( actualPage, distDir, @@ -554,10 +555,11 @@ export default async function build(dir: string, conf = null): Promise { routesManifest.serverPropsRoutes = {} for (const page of serverPropsPages) { + const normalizedPage = normalizePagePath(page) const dataRoute = path.posix.join( '/_next/data', buildId, - `${page === '/' ? '/index' : page}.json` + `${normalizedPage}.json` ) routesManifest.serverPropsRoutes[page] = { @@ -571,7 +573,7 @@ export default async function build(dir: string, conf = null): Promise { `^${path.posix.join( '/_next/data', escapeStringRegexp(buildId), - `${page === '/' ? '/index' : page}.json` + `${normalizedPage}.json` )}$` ).source, } @@ -637,15 +639,17 @@ export default async function build(dir: string, conf = null): Promise { // n.b. we cannot handle this above in combinedPages because the dynamic // page must be in the `pages` array, but not in the mapping. exportPathMap: (defaultMap: any) => { - // Remove dynamically routed pages from the default path map. These - // pages cannot be prerendered because we don't have enough information - // to do so. + // Generate fallback for dynamically routed pages to use as + // the loading state for pages while the data is being populated // // Note: prerendering disables automatic static optimization. ssgPages.forEach(page => { if (isDynamicRoute(page)) { tbdPrerenderRoutes.push(page) - delete defaultMap[page] + // set __nextFallback query so render doesn't call + // getStaticProps/getServerProps + defaultMap[page].query = { __nextFallback: true } + console.log('set fallback page') } }) // Append the "well-known" routes we should prerender for, e.g. blog @@ -709,13 +713,12 @@ export default async function build(dir: string, conf = null): Promise { for (const page of combinedPages) { const isSsg = ssgPages.has(page) const isDynamic = isDynamicRoute(page) - let file = page === '/' ? '/index' : page - // The dynamic version of SSG pages are not prerendered. Below, we handle - // the specific prerenders of these. - if (!(isSsg && isDynamic)) { - await moveExportedPage(page, file, isSsg, 'html') - } 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') + if (hasAmp) { await moveExportedPage(`${page}.amp`, `${file}.amp`, isSsg, 'html') } @@ -729,15 +732,12 @@ export default async function build(dir: string, conf = null): Promise { initialRevalidateSeconds: exportConfig.initialPageRevalidationMap[page], srcRoute: null, - dataRoute: path.posix.join( - '/_next/data', - buildId, - `${page === '/' ? '/index' : page}.json` - ), + dataRoute: path.posix.join('/_next/data', buildId, `${file}.json`), } } else { - // For a dynamic SSG page, we did not copy its html nor data exports. - // Instead, we must copy specific versions of this page as defined by + // For a dynamic SSG page, we did not copy its data exports and only + // copy the fallback HTML file. + // We must also copy specific versions of this page as defined by // `unstable_getStaticPaths` (additionalSsgPaths). const extraRoutes = additionalSsgPaths.get(page) || [] for (const route of extraRoutes) { @@ -750,7 +750,7 @@ export default async function build(dir: string, conf = null): Promise { dataRoute: path.posix.join( '/_next/data', buildId, - `${route === '/' ? '/index' : route}.json` + `${normalizePagePath(route)}.json` ), } } @@ -780,15 +780,17 @@ export default async function build(dir: string, conf = null): Promise { if (ssgPages.size > 0) { const finalDynamicRoutes: PrerenderManifest['dynamicRoutes'] = {} tbdPrerenderRoutes.forEach(tbdRoute => { + const normalizedRoute = normalizePagePath(tbdRoute) const dataRoute = path.posix.join( '/_next/data', buildId, - `${tbdRoute === '/' ? '/index' : tbdRoute}.json` + `${normalizedRoute}.json` ) finalDynamicRoutes[tbdRoute] = { routeRegex: getRouteRegex(tbdRoute).re.source, dataRoute, + fallback: `${normalizedRoute}.html`, dataRouteRegex: getRouteRegex( dataRoute.replace(/\.json$/, '') ).re.source.replace(/\(\?:\\\/\)\?\$$/, '\\.json$'), diff --git a/packages/next/build/ssg.ts b/packages/next/build/ssg.ts new file mode 100644 index 0000000000000..ec430189eb2c4 --- /dev/null +++ b/packages/next/build/ssg.ts @@ -0,0 +1,76 @@ +import { ParsedUrlQuery } from 'querystring' +import { getRouteRegex, getRouteMatcher } from '../next-server/lib/router/utils' +import { Unstable_getStaticPaths } from '../next-server/server/load-components' + +export async function getPrerenderPaths( + page: string, + unstable_getStaticPaths: Unstable_getStaticPaths +) { + const prerenderPaths = [] as string[] + + const _routeRegex = getRouteRegex(page) + const _routeMatcher = getRouteMatcher(_routeRegex) + + // Get the default list of allowed params. + const _validParamKeys = Object.keys(_routeMatcher(page)) + const toPrerender = await unstable_getStaticPaths() + + toPrerender.forEach(entry => { + // For a string-provided path, we must make sure it matches the dynamic + // route. + if (typeof entry === 'string') { + const result = _routeMatcher(entry) + if (!result) { + throw new Error( + `The provided path \`${entry}\` does not match the page: \`${page}\`.` + ) + } + + prerenderPaths!.push(entry) + } + // For the object-provided path, we must make sure it specifies all + // required keys. + else { + const invalidKeys = Object.keys(entry).filter(key => key !== 'params') + if (invalidKeys.length) { + throw new Error( + `Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` + + `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + + `\n\n\treturn { params: { ${_validParamKeys + .map(k => `${k}: ...`) + .join(', ')} } }` + + `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` + ) + } + + const { params = {} }: { params?: ParsedUrlQuery } = entry + let builtPage = page + + _validParamKeys.forEach(validParamKey => { + const { repeat } = _routeRegex.groups[validParamKey] + const paramValue = params[validParamKey] + if ( + (repeat && !Array.isArray(paramValue)) || + (!repeat && typeof paramValue !== 'string') + ) { + throw new Error( + `A required parameter (${validParamKey}) was not provided as ${ + repeat ? 'an array' : 'a string' + }.` + ) + } + + builtPage = builtPage.replace( + `[${repeat ? '...' : ''}${validParamKey}]`, + repeat + ? (paramValue as string[]).map(encodeURIComponent).join('/') + : encodeURIComponent(paramValue as string) + ) + }) + + prerenderPaths!.push(builtPage) + } + }) + + return prerenderPaths +} diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 3c68b3d548592..1e793cc85bb02 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -16,9 +16,9 @@ import { } from '../lib/constants' import prettyBytes from '../lib/pretty-bytes' import { recursiveReadDir } from '../lib/recursive-readdir' -import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils' import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic' import { findPageFile } from '../server/lib/find-page-file' +import { getPrerenderPaths } from './ssg' const fileGzipStats: { [k: string]: Promise } = {} const fsStatGzip = (file: string) => { @@ -533,74 +533,10 @@ export async function isPageStatic( let prerenderPaths: string[] | undefined if (hasStaticProps && hasStaticPaths) { - prerenderPaths = [] as string[] - - const _routeRegex = getRouteRegex(page) - const _routeMatcher = getRouteMatcher(_routeRegex) - - // Get the default list of allowed params. - const _validParamKeys = Object.keys(_routeMatcher(page)) - - const toPrerender: Array< - { params?: { [key: string]: string } } | string - > = await mod.unstable_getStaticPaths() - toPrerender.forEach(entry => { - // For a string-provided path, we must make sure it matches the dynamic - // route. - if (typeof entry === 'string') { - const result = _routeMatcher(entry) - if (!result) { - throw new Error( - `The provided path \`${entry}\` does not match the page: \`${page}\`.` - ) - } - - prerenderPaths!.push(entry) - } - // For the object-provided path, we must make sure it specifies all - // required keys. - else { - const invalidKeys = Object.keys(entry).filter(key => key !== 'params') - if (invalidKeys.length) { - throw new Error( - `Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` + - `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + - `\n\n\treturn { params: { ${_validParamKeys - .map(k => `${k}: ...`) - .join(', ')} } }` + - `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` - ) - } - - const { params = {} } = entry - let builtPage = page - _validParamKeys.forEach(validParamKey => { - 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 ${ - repeat ? 'an array' : 'a string' - }.` - ) - } - - builtPage = builtPage.replace( - `[${repeat ? '...' : ''}${validParamKey}]`, - repeat - ? (paramValue as string[]).map(encodeURIComponent).join('/') - : encodeURIComponent(paramValue as string) - ) - }) - - prerenderPaths!.push(builtPage) - } - }) + prerenderPaths = await getPrerenderPaths( + page, + mod.unstable_getStaticPaths + ) } const config = mod.config || {} diff --git a/packages/next/client/index.js b/packages/next/client/index.js index 60e64d0cdf7fe..c75f3b9c3980c 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -203,6 +203,12 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { }, }) + // call router.replace to trigger data fetching while + // the fallback is shown + if (data.isFallback) { + router.replace({ pathname: page, query }, asPath) + } + // call init-client middleware if (process.env.__NEXT_PLUGINS) { // eslint-disable-next-line diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 4c5a30545b839..2abe2fc3d38a4 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -150,7 +150,6 @@ export default class Router implements BaseRouter { // Backwards compat for Router.router.events // TODO: Should be remove the following major version as it was never documented - // @ts-ignore backwards compatibility this.events = Router.events this.pageLoader = pageLoader @@ -355,7 +354,6 @@ export default class Router implements BaseRouter { method = 'replaceState' } - // @ts-ignore pathname is always a string const route = toRoute(pathname) const { shallow = false } = options @@ -394,7 +392,6 @@ export default class Router implements BaseRouter { Router.events.emit('routeChangeStart', as) // If shallow is true and the route exists in the router cache we reuse the previous result - // @ts-ignore pathname is always a string this.getRouteInfo(route, pathname, query, as, shallow).then(routeInfo => { const { error } = routeInfo @@ -404,7 +401,6 @@ export default class Router implements BaseRouter { Router.events.emit('beforeHistoryChange', as) this.changeState(method, url, addBasePath(as), options) - const hash = window.location.hash.substring(1) if (process.env.NODE_ENV !== 'production') { const appComp: any = this.components['/_app'].Component @@ -413,8 +409,7 @@ export default class Router implements BaseRouter { !(routeInfo.Component as any).getInitialProps } - // @ts-ignore pathname is always defined - this.set(route, pathname, query, as, { ...routeInfo, hash }) + this.set(route, pathname, query, as, routeInfo) if (error) { Router.events.emit('routeChangeError', error, as) @@ -422,7 +417,20 @@ export default class Router implements BaseRouter { } Router.events.emit('routeChangeComplete', as) - return resolve(true) + + if ((routeInfo as any).dataRes) { + return ((routeInfo as any).dataRes as Promise).then( + routeInfo => { + this.set(route, pathname, query, as, routeInfo) + // TODO: should we resolve when the fallback is rendered + // or when the data is returned and we render with data? + resolve(true) + }, + reject + ) + } else { + return resolve(true) + } }, reject) }) } @@ -491,20 +499,43 @@ export default class Router implements BaseRouter { } } - return this._getData(() => - (Component as any).__N_SSG + const isSSG = (Component as any).__N_SSG + const isSSP = (Component as any).__N_SSG + + const handleData = (props: any) => { + routeInfo.props = props + this.components[route] = routeInfo + return routeInfo + } + + // if we have data in cache resolve with the data + // if not we we resolve with fallback routeInfo + if (isSSG || isSSP) { + const dataRes = isSSG ? this._getStaticData(as) - : (Component as any).__N_SSP - ? this._getServerData(as) - : this.getInitialProps( - Component, - // we provide AppTree later so this needs to be `any` - { - pathname, - query, - asPath: as, - } as any - ) + : this._getServerData(as) + + return Promise.resolve( + typeof dataRes.then !== 'function' + ? handleData(dataRes) + : { + ...routeInfo, + props: {}, + dataRes: dataRes.then((props: any) => handleData(props)), + } + ) + } + + return this._getData(() => + this.getInitialProps( + Component, + // we provide AppTree later so this needs to be `any` + { + pathname, + query, + asPath: as, + } as any + ) ).then(props => { routeInfo.props = props this.components[route] = routeInfo @@ -658,7 +689,6 @@ export default class Router implements BaseRouter { return } - // @ts-ignore pathname is always defined const route = toRoute(pathname) this.pageLoader.prefetch(route).then(resolve, reject) }) @@ -708,11 +738,11 @@ export default class Router implements BaseRouter { }) } - _getStaticData = (asPath: string): Promise => { + _getStaticData = (asPath: string): Promise | any => { const pathname = prepareRoute(parse(asPath).pathname!) return process.env.NODE_ENV === 'production' && this.sdc[pathname] - ? Promise.resolve(this.sdc[pathname]) + ? this.sdc[pathname] : fetchNextData(pathname, null, data => (this.sdc[pathname] = data)) } diff --git a/packages/next/next-server/lib/utils.ts b/packages/next/next-server/lib/utils.ts index 875e8417a7332..f1adc16a117f8 100644 --- a/packages/next/next-server/lib/utils.ts +++ b/packages/next/next-server/lib/utils.ts @@ -76,6 +76,7 @@ export type NEXT_DATA = { runtimeConfig?: { [key: string]: any } nextExport?: boolean autoExport?: boolean + isFallback?: boolean dynamicIds?: string[] err?: Error & { statusCode?: number } } diff --git a/packages/next/next-server/server/load-components.ts b/packages/next/next-server/server/load-components.ts index 78995e867d3db..8a935992513f8 100644 --- a/packages/next/next-server/server/load-components.ts +++ b/packages/next/next-server/server/load-components.ts @@ -32,7 +32,9 @@ type Unstable_getStaticProps = (params: { revalidate?: number | boolean }> -type Unstable_getStaticPaths = () => Promise> +export type Unstable_getStaticPaths = () => Promise< + Array +> type Unstable_getServerProps = (context: { params: ParsedUrlQuery | undefined diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index c817e6890e65c..24cf96254bd39 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -52,6 +52,7 @@ import { Header, getRedirectStatus, } from '../../lib/check-custom-routes' +import { getPrerenderPaths } from '../../build/ssg' const getCustomRouteMatcher = pathMatch(true) @@ -1009,6 +1010,39 @@ export default class Server { return { html, pageData, sprRevalidate } }) + // render fallback if cached data wasn't available in prod + // and the page isn't specified in getStaticPaths in dev mode + let skipFallback = false + + if (this.renderOpts.dev && result.unstable_getStaticPaths) { + const prerenderPaths = await getPrerenderPaths( + pathname, + result.unstable_getStaticPaths + ) + + skipFallback = prerenderPaths.includes(req.url || '') + } + + if ( + !skipFallback && + !isResSent(res) && + !isDataReq && + isDynamicRoute(pathname) + ) { + query.__nextFallback = 'true' + let html = '' + if (isLikeServerless) { + this.prepareServerlessUrl(req, query) + html = await (result.Component as any).renderReqToHTML(req, res) + } + html = (await renderToHTML(req, res, pathname, query, { + ...result, + ...opts, + })) as string + + this.__sendPayload(res, html, 'text/html; charset=utf-8') + } + return doRender(ssgCacheKey, []).then( async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => { // Respond to the request if a payload wasn't sent above (from cache) diff --git a/packages/next/next-server/server/render.tsx b/packages/next/next-server/server/render.tsx index 50eaad0e69ab2..5227476196bf7 100644 --- a/packages/next/next-server/server/render.tsx +++ b/packages/next/next-server/server/render.tsx @@ -152,6 +152,7 @@ function renderDocument( runtimeConfig, nextExport, autoExport, + isFallback, dynamicImportsIds, dangerousAsPath, hasCssMode, @@ -187,6 +188,7 @@ function renderDocument( htmlProps: any bodyTags: any headTags: any + isFallback?: boolean } ): string { return ( @@ -203,6 +205,7 @@ function renderDocument( runtimeConfig, // runtimeConfig if provided, otherwise don't sent in the resulting HTML nextExport, // If this is a page exported by `next export` autoExport, // If this is an auto exported page + isFallback, dynamicIds: dynamicImportsIds.length === 0 ? undefined : dynamicImportsIds, err: err ? serializeError(dev, err) : undefined, // Error if one happened, otherwise don't sent in the resulting HTML @@ -294,6 +297,9 @@ export async function renderToHTML( const bodyTags = (...args: any) => callMiddleware('bodyTags', args) const htmlProps = (...args: any) => callMiddleware('htmlProps', args, true) + const isFallback = !!query.__nextFallback + delete query.__nextFallback + const isSpr = !!unstable_getStaticProps const defaultAppGetInitialProps = App.getInitialProps === (App as any).origGetInitialProps @@ -428,7 +434,7 @@ export async function renderToHTML( ctx, }) - if (isSpr) { + if (isSpr && !isFallback) { const data = await unstable_getStaticProps!({ params: isDynamicRoute(pathname) ? (query as any) : undefined, }) @@ -483,7 +489,7 @@ export async function renderToHTML( renderOpts.err = err } - if (unstable_getServerProps) { + if (unstable_getServerProps && !isFallback) { const data = await unstable_getServerProps({ params, query, @@ -504,6 +510,12 @@ export async function renderToHTML( // _app's getInitialProps for getServerProps if not this can be removed if (isDataReq) return props + // We don't call getStaticProps or getServerProps while generating + // the fallback so make sure to set pageProps to an empty object + if (isFallback) { + props.pageProps = {} + } + // the response might be finished on the getInitialProps call if (isResSent(res) && !isSpr) return null @@ -600,6 +612,7 @@ export async function renderToHTML( headTags: await headTags(documentCtx), bodyTags: await bodyTags(documentCtx), htmlProps: await htmlProps(documentCtx), + isFallback, docProps, pathname, ampPath, diff --git a/test/integration/prerender/pages/blog/[post]/[comment].js b/test/integration/prerender/pages/blog/[post]/[comment].js index 5a2473348ccc7..5c74b494c960e 100644 --- a/test/integration/prerender/pages/blog/[post]/[comment].js +++ b/test/integration/prerender/pages/blog/[post]/[comment].js @@ -11,6 +11,8 @@ export async function unstable_getStaticPaths() { // eslint-disable-next-line camelcase export async function unstable_getStaticProps({ params }) { + await new Promise(resolve => setTimeout(resolve, 500)) + return { props: { post: params.post, @@ -22,6 +24,11 @@ export async function unstable_getStaticProps({ params }) { } export default ({ post, comment, time }) => { + // we're in a loading state + if (!post) { + return

loading...

+ } + return ( <>

Post: {post}

diff --git a/test/integration/prerender/pages/catchall/[...slug].js b/test/integration/prerender/pages/catchall/[...slug].js index 4727b5551a84f..4a01f9e698076 100644 --- a/test/integration/prerender/pages/catchall/[...slug].js +++ b/test/integration/prerender/pages/catchall/[...slug].js @@ -16,4 +16,4 @@ export async function unstable_getStaticPaths() { ] } -export default ({ slug }) =>

Hi {slug.join('/')}

+export default ({ slug }) =>

Hi {slug?.join('/')}

diff --git a/yarn.lock b/yarn.lock index 344301d01b2c4..11c5f42701ef9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4133,7 +4133,7 @@ browserify-zlib@^0.2.0: dependencies: pako "~1.0.5" -browserslist@4.8.3, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.0, browserslist@^4.8.2, browserslist@^4.8.3: +browserslist@4.8.3, browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6.0, browserslist@^4.6.4, browserslist@^4.8.0, browserslist@^4.8.2, browserslist@^4.8.3: version "4.8.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.8.3.tgz#65802fcd77177c878e015f0e3189f2c4f627ba44" integrity sha512-iU43cMMknxG1ClEZ2MDKeonKE1CCrFVkQK2AqO2YWFmvIrx4JWrvQ4w4hQez6EpVI8rHTtqh/ruHHDHSOKxvUg== @@ -4142,14 +4142,6 @@ browserslist@4.8.3, browserslist@^4.0.0, browserslist@^4.3.6, browserslist@^4.6. electron-to-chromium "^1.3.322" node-releases "^1.1.44" -browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: - version "1.7.7" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-1.7.7.tgz#0bd76704258be829b2398bb50e4b62d1a166b0b9" - integrity sha1-C9dnBCWL6CmyOYu1Dkti0aFmsLk= - dependencies: - caniuse-db "^1.0.30000639" - electron-to-chromium "^1.2.7" - browserstack-local@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.4.0.tgz#d979cac056f57b9af159b3bcd7fdc09b4354537c" @@ -4448,21 +4440,11 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634: resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001023.tgz#f856f71af16a5a44e81f1fcefc1673912a43da72" integrity sha512-EnlshvE6oAum+wWwKmJNVaoqJMjIc0bLUy4Dj77VVnz1o6bzSPr1Ze9iPy6g5ycg1xD6jGU6vBmo7pLEz2MbCQ== -caniuse-db@^1.0.30000639: - version "1.0.30001025" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001025.tgz#33b6b126f3070a54fa8017bb5c4a0fd6b787d6f1" - integrity sha512-HtUBOYgagTFMOa8/OSVkXbDS/YiByZZoi4H+ksKgoDfNmMVoodxnH373bXleumM1kg1IXvLspLMKIS7guWEBhg== - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001012, caniuse-lite@^1.0.30001017, caniuse-lite@^1.0.30001019: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001012, caniuse-lite@^1.0.30001017, caniuse-lite@^1.0.30001019, caniuse-lite@^1.0.30001020: version "1.0.30001019" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001019.tgz#857e3fccaad2b2feb3f1f6d8a8f62d747ea648e1" integrity sha512-6ljkLtF1KM5fQ+5ZN0wuyVvvebJxgJPTmScOMaFuQN2QuOzvRJnWSKfzQskQU5IOU4Gap3zasYPIinzwUjoj/g== -caniuse-lite@^1.0.30001020: - version "1.0.30001025" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001025.tgz#30336a8aca7f98618eb3cf38e35184e13d4e5fe6" - integrity sha512-SKyFdHYfXUZf5V85+PJgLYyit27q4wgvZuf8QTOk1osbypcROihMBlx9GRar2/pIcKH2r4OehdlBr9x6PXetAQ== - capitalize@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/capitalize/-/capitalize-1.0.0.tgz#dc802c580aee101929020d2ca14b4ca8a0ae44be" @@ -6220,11 +6202,6 @@ ejs@^2.6.1: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.7.4.tgz#48661287573dcc53e366c7a1ae52c3a120eec9ba" integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA== -electron-to-chromium@^1.2.7: - version "1.3.345" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.345.tgz#2569d0d54a64ef0f32a4b7e8c80afa5fe57c5d98" - integrity sha512-f8nx53+Z9Y+SPWGg3YdHrbYYfIJAtbUjpFfW4X1RwTZ94iUG7geg9tV8HqzAXX7XTNgyWgAFvce4yce8ZKxKmg== - electron-to-chromium@^1.3.322: version "1.3.327" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.327.tgz#516f28b4271727004362b4ac814494ae64d9dde7" From c20da129940081efd2606deb5186a7e998be18ff Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Feb 2020 22:24:43 -0600 Subject: [PATCH 02/17] Remove extra changes and update fallback handling --- packages/next/build/index.ts | 3 +- packages/next/build/ssg.ts | 76 ------------------- packages/next/build/utils.ts | 74 ++++++++++++++++-- packages/next/client/index.js | 31 ++++++-- .../next/next-server/lib/router/router.ts | 13 ++-- .../next-server/server/load-components.ts | 4 +- .../next/next-server/server/next-server.ts | 31 ++------ test/integration/prerender/test/index.test.js | 53 ++++++++++--- 8 files changed, 153 insertions(+), 132 deletions(-) delete mode 100644 packages/next/build/ssg.ts diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 0a988502097b3..be20d026a8553 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -648,8 +648,7 @@ export default async function build(dir: string, conf = null): Promise { tbdPrerenderRoutes.push(page) // set __nextFallback query so render doesn't call // getStaticProps/getServerProps - defaultMap[page].query = { __nextFallback: true } - console.log('set fallback page') + defaultMap[page] = { page, query: { __nextFallback: true } } } }) // Append the "well-known" routes we should prerender for, e.g. blog diff --git a/packages/next/build/ssg.ts b/packages/next/build/ssg.ts deleted file mode 100644 index ec430189eb2c4..0000000000000 --- a/packages/next/build/ssg.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { ParsedUrlQuery } from 'querystring' -import { getRouteRegex, getRouteMatcher } from '../next-server/lib/router/utils' -import { Unstable_getStaticPaths } from '../next-server/server/load-components' - -export async function getPrerenderPaths( - page: string, - unstable_getStaticPaths: Unstable_getStaticPaths -) { - const prerenderPaths = [] as string[] - - const _routeRegex = getRouteRegex(page) - const _routeMatcher = getRouteMatcher(_routeRegex) - - // Get the default list of allowed params. - const _validParamKeys = Object.keys(_routeMatcher(page)) - const toPrerender = await unstable_getStaticPaths() - - toPrerender.forEach(entry => { - // For a string-provided path, we must make sure it matches the dynamic - // route. - if (typeof entry === 'string') { - const result = _routeMatcher(entry) - if (!result) { - throw new Error( - `The provided path \`${entry}\` does not match the page: \`${page}\`.` - ) - } - - prerenderPaths!.push(entry) - } - // For the object-provided path, we must make sure it specifies all - // required keys. - else { - const invalidKeys = Object.keys(entry).filter(key => key !== 'params') - if (invalidKeys.length) { - throw new Error( - `Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` + - `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + - `\n\n\treturn { params: { ${_validParamKeys - .map(k => `${k}: ...`) - .join(', ')} } }` + - `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` - ) - } - - const { params = {} }: { params?: ParsedUrlQuery } = entry - let builtPage = page - - _validParamKeys.forEach(validParamKey => { - const { repeat } = _routeRegex.groups[validParamKey] - const paramValue = params[validParamKey] - if ( - (repeat && !Array.isArray(paramValue)) || - (!repeat && typeof paramValue !== 'string') - ) { - throw new Error( - `A required parameter (${validParamKey}) was not provided as ${ - repeat ? 'an array' : 'a string' - }.` - ) - } - - builtPage = builtPage.replace( - `[${repeat ? '...' : ''}${validParamKey}]`, - repeat - ? (paramValue as string[]).map(encodeURIComponent).join('/') - : encodeURIComponent(paramValue as string) - ) - }) - - prerenderPaths!.push(builtPage) - } - }) - - return prerenderPaths -} diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 1e793cc85bb02..3c68b3d548592 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -16,9 +16,9 @@ import { } from '../lib/constants' import prettyBytes from '../lib/pretty-bytes' import { recursiveReadDir } from '../lib/recursive-readdir' +import { getRouteMatcher, getRouteRegex } from '../next-server/lib/router/utils' import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic' import { findPageFile } from '../server/lib/find-page-file' -import { getPrerenderPaths } from './ssg' const fileGzipStats: { [k: string]: Promise } = {} const fsStatGzip = (file: string) => { @@ -533,10 +533,74 @@ export async function isPageStatic( let prerenderPaths: string[] | undefined if (hasStaticProps && hasStaticPaths) { - prerenderPaths = await getPrerenderPaths( - page, - mod.unstable_getStaticPaths - ) + prerenderPaths = [] as string[] + + const _routeRegex = getRouteRegex(page) + const _routeMatcher = getRouteMatcher(_routeRegex) + + // Get the default list of allowed params. + const _validParamKeys = Object.keys(_routeMatcher(page)) + + const toPrerender: Array< + { params?: { [key: string]: string } } | string + > = await mod.unstable_getStaticPaths() + toPrerender.forEach(entry => { + // For a string-provided path, we must make sure it matches the dynamic + // route. + if (typeof entry === 'string') { + const result = _routeMatcher(entry) + if (!result) { + throw new Error( + `The provided path \`${entry}\` does not match the page: \`${page}\`.` + ) + } + + prerenderPaths!.push(entry) + } + // For the object-provided path, we must make sure it specifies all + // required keys. + else { + const invalidKeys = Object.keys(entry).filter(key => key !== 'params') + if (invalidKeys.length) { + throw new Error( + `Additional keys were returned from \`unstable_getStaticPaths\` in page "${page}". ` + + `URL Parameters intended for this dynamic route must be nested under the \`params\` key, i.e.:` + + `\n\n\treturn { params: { ${_validParamKeys + .map(k => `${k}: ...`) + .join(', ')} } }` + + `\n\nKeys that need to be moved: ${invalidKeys.join(', ')}.\n` + ) + } + + const { params = {} } = entry + let builtPage = page + _validParamKeys.forEach(validParamKey => { + 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 ${ + repeat ? 'an array' : 'a string' + }.` + ) + } + + builtPage = builtPage.replace( + `[${repeat ? '...' : ''}${validParamKey}]`, + repeat + ? (paramValue as string[]).map(encodeURIComponent).join('/') + : encodeURIComponent(paramValue as string) + ) + }) + + prerenderPaths!.push(builtPage) + } + }) } const config = mod.config || {} diff --git a/packages/next/client/index.js b/packages/next/client/index.js index c75f3b9c3980c..8f5618f770b7e 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -96,9 +96,28 @@ class Container extends React.Component { }) } + // call router.replace to trigger data fetching while + // the fallback is shown + + if (data.isFallback) { + router.replace( + { + pathname: page, + query: { + ...router.query, + ...parseQs(location.search.substr(1)), + }, + }, + asPath, + { _h: 1 } + ) + } + // If page was exported and has a querystring // If it's a dynamic route or has a querystring + if ( + !data.isFallback && router.isSsr && ((data.nextExport && (isDynamicRoute(router.pathname) || location.search)) || @@ -203,12 +222,6 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { }, }) - // call router.replace to trigger data fetching while - // the fallback is shown - if (data.isFallback) { - router.replace({ pathname: page, query }, asPath) - } - // call init-client middleware if (process.env.__NEXT_PLUGINS) { // eslint-disable-next-line @@ -224,6 +237,12 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { const renderCtx = { App, Component, props, err: initialErr } if (process.env.NODE_ENV === 'production') { + // kick off static data request now so it's in the cache + // when we re-render post-hydration + if (data.isFallback) { + router._getStaticData(asPath).catch(() => {}) + } + render(renderCtx) return emitter } diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 2abe2fc3d38a4..0925cd5643dc9 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -500,7 +500,7 @@ export default class Router implements BaseRouter { } const isSSG = (Component as any).__N_SSG - const isSSP = (Component as any).__N_SSG + const isSSP = (Component as any).__N_SSP const handleData = (props: any) => { routeInfo.props = props @@ -510,6 +510,7 @@ export default class Router implements BaseRouter { // if we have data in cache resolve with the data // if not we we resolve with fallback routeInfo + if (isSSG || isSSP) { const dataRes = isSSG ? this._getStaticData(as) @@ -521,7 +522,9 @@ export default class Router implements BaseRouter { : { ...routeInfo, props: {}, - dataRes: dataRes.then((props: any) => handleData(props)), + dataRes: this._getData(() => + dataRes.then((props: any) => handleData(props)) + ), } ) } @@ -536,11 +539,7 @@ export default class Router implements BaseRouter { asPath: as, } as any ) - ).then(props => { - routeInfo.props = props - this.components[route] = routeInfo - return routeInfo - }) + ).then(props => handleData(props)) }) .catch(err => { return new Promise(resolve => { diff --git a/packages/next/next-server/server/load-components.ts b/packages/next/next-server/server/load-components.ts index 8a935992513f8..78995e867d3db 100644 --- a/packages/next/next-server/server/load-components.ts +++ b/packages/next/next-server/server/load-components.ts @@ -32,9 +32,7 @@ type Unstable_getStaticProps = (params: { revalidate?: number | boolean }> -export type Unstable_getStaticPaths = () => Promise< - Array -> +type Unstable_getStaticPaths = () => Promise> type Unstable_getServerProps = (context: { params: ParsedUrlQuery | undefined diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 24cf96254bd39..47347ee411fc1 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -52,7 +52,6 @@ import { Header, getRedirectStatus, } from '../../lib/check-custom-routes' -import { getPrerenderPaths } from '../../build/ssg' const getCustomRouteMatcher = pathMatch(true) @@ -1010,35 +1009,19 @@ export default class Server { return { html, pageData, sprRevalidate } }) - // render fallback if cached data wasn't available in prod - // and the page isn't specified in getStaticPaths in dev mode - let skipFallback = false - - if (this.renderOpts.dev && result.unstable_getStaticPaths) { - const prerenderPaths = await getPrerenderPaths( - pathname, - result.unstable_getStaticPaths - ) - - skipFallback = prerenderPaths.includes(req.url || '') - } - - if ( - !skipFallback && - !isResSent(res) && - !isDataReq && - isDynamicRoute(pathname) - ) { + // render fallback if cached data wasn't available + if (!isResSent(res) && !isDataReq && isDynamicRoute(pathname)) { query.__nextFallback = 'true' let html = '' if (isLikeServerless) { this.prepareServerlessUrl(req, query) html = await (result.Component as any).renderReqToHTML(req, res) + } else { + html = (await renderToHTML(req, res, pathname, query, { + ...result, + ...opts, + })) as string } - html = (await renderToHTML(req, res, pathname, query, { - ...result, - ...opts, - })) as string this.__sendPayload(res, html, 'text/html; charset=utf-8') } diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index a6a6fc5ec63e4..a1bad06c53b7c 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -253,7 +253,18 @@ const runTests = (dev = false) => { it('should SSR SPR page correctly', async () => { const html = await renderViaHTTP(appPort, '/blog/post-1') - expect(html).toMatch(/Post:.*?post-1/) + + if (dev) { + expect( + JSON.parse( + cheerio + .load(html)('#__NEXT_DATA__') + .text() + ).isFallback + ).toBe(true) + } else { + expect(html).toMatch(/Post:.*?post-1/) + } }) it('should not supply query values to params or useRouter non-dynamic page SSR', async () => { @@ -278,8 +289,13 @@ const runTests = (dev = false) => { it('should not supply query values to params or useRouter dynamic page SSR', async () => { const html = await renderViaHTTP(appPort, '/blog/post-1?hello=world') const $ = cheerio.load(html) - const params = $('#params').text() - expect(JSON.parse(params)).toEqual({ post: 'post-1' }) + + if (!dev) { + // these aren't available in dev since we render the fallback always + const params = $('#params').text() + expect(JSON.parse(params)).toEqual({ post: 'post-1' }) + } + const query = $('#query').text() expect(JSON.parse(query)).toEqual({ post: 'post-1' }) }) @@ -346,13 +362,24 @@ const runTests = (dev = false) => { 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/) + + if (dev) { + expect( + JSON.parse( + cheerio + .load(html)('#__NEXT_DATA__') + .text() + ).isFallback + ).toBe(true) + } else { + 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/) + const browser = await webdriver(appPort, '/catchall/third') + const text = await browser.elementByCss('#catchall').text() + expect(text).toMatch(/Hi.*?third/) }) if (dev) { @@ -438,6 +465,7 @@ const runTests = (dev = false) => { expect(manifest.routes).toEqual(expectedManifestRoutes()) expect(manifest.dynamicRoutes).toEqual({ '/blog/[post]': { + fallback: '/blog/[post].html', dataRoute: `/_next/data/${buildId}/blog/[post].json`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapedBuildId}\\/blog\\/([^\\/]+?)\\.json$` @@ -445,6 +473,7 @@ const runTests = (dev = false) => { routeRegex: normalizeRegEx('^\\/blog\\/([^\\/]+?)(?:\\/)?$'), }, '/blog/[post]/[comment]': { + fallback: '/blog/[post]/[comment].html', dataRoute: `/_next/data/${buildId}/blog/[post]/[comment].json`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapedBuildId}\\/blog\\/([^\\/]+?)\\/([^\\/]+?)\\.json$` @@ -454,6 +483,7 @@ const runTests = (dev = false) => { ), }, '/user/[user]/profile': { + fallback: '/user/[user]/profile.html', dataRoute: `/_next/data/${buildId}/user/[user]/profile.json`, dataRouteRegex: normalizeRegEx( `^\\/_next\\/data\\/${escapedBuildId}\\/user\\/([^\\/]+?)\\/profile\\.json$` @@ -463,6 +493,7 @@ const runTests = (dev = false) => { ), }, '/catchall/[...slug]': { + fallback: '/catchall/[...slug].html', routeRegex: normalizeRegEx('^\\/catchall\\/(.+?)(?:\\/)?$'), dataRoute: `/_next/data/${buildId}/catchall/[...slug].json`, dataRouteRegex: normalizeRegEx( @@ -489,11 +520,15 @@ const runTests = (dev = false) => { it('should handle de-duping correctly', async () => { let vals = new Array(10).fill(null) + // use data route so we don't get the fallback vals = await Promise.all( - vals.map(() => renderViaHTTP(appPort, '/blog/post-10')) + vals.map(() => + renderViaHTTP(appPort, `/_next/data/${buildId}/blog/post-10.json`) + ) ) const val = vals[0] - expect(val).toMatch(/Post:.*?post-10/) + + expect(JSON.parse(val).pageProps.post).toBe('post-10') expect(new Set(vals).size).toBe(1) }) From 99188d340f9d55062bc8770efaf28cf8d63c139e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Feb 2020 22:32:32 -0600 Subject: [PATCH 03/17] Remove extra timeout for testing --- test/integration/prerender/pages/blog/[post]/[comment].js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/prerender/pages/blog/[post]/[comment].js b/test/integration/prerender/pages/blog/[post]/[comment].js index 5c74b494c960e..9d49339ca9b49 100644 --- a/test/integration/prerender/pages/blog/[post]/[comment].js +++ b/test/integration/prerender/pages/blog/[post]/[comment].js @@ -11,8 +11,6 @@ export async function unstable_getStaticPaths() { // eslint-disable-next-line camelcase export async function unstable_getStaticProps({ params }) { - await new Promise(resolve => setTimeout(resolve, 500)) - return { props: { post: params.post, From 9a8e7108b61137ea24886226c963402ca203949d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 4 Feb 2020 23:04:29 -0600 Subject: [PATCH 04/17] Update SSG tests in dynamic-routing suite --- .../dynamic-routing/test/index.test.js | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 756f472323676..375092823b0e0 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -239,48 +239,58 @@ function runTests(dev) { } }) - it('[ssg: catch all] should pass param in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssg/test1') - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe('{"rest":["test1"]}') + it('[ssg: catch all] should pass param in getStaticProps during SSR', async () => { + const data = await renderViaHTTP( + appPort, + `/_next/data/${buildId}/p1/p2/all-ssg/test1.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) }) - it('[ssg: catch all] should pass params in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/all-ssg/test1/test2') - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe('{"rest":["test1","test2"]}') + it('[ssg: catch all] should pass params in getStaticProps during SSR', async () => { + const data = await renderViaHTTP( + appPort, + `/_next/data/${buildId}/p1/p2/all-ssg/test1/test2.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['test1', 'test2'], + }) }) - it('[predefined ssg: catch all] should pass param in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/predefined-ssg/test1') - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe('{"rest":["test1"]}') + it('[predefined ssg: catch all] should pass param in getStaticProps during SSR', async () => { + const data = await renderViaHTTP( + appPort, + `/_next/data/${buildId}/p1/p2/predefined-ssg/test1.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['test1'] }) }) - it('[predefined ssg: catch all] should pass params in getInitialProps during SSR', async () => { - const html = await renderViaHTTP( + it('[predefined ssg: catch all] should pass params in getStaticProps during SSR', async () => { + const data = await renderViaHTTP( appPort, - '/p1/p2/predefined-ssg/test1/test2' + `/_next/data/${buildId}/p1/p2/predefined-ssg/test1/test2.json` ) - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe('{"rest":["test1","test2"]}') + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['test1', 'test2'], + }) }) it('[predefined ssg: prerendered catch all] should pass param in getInitialProps during SSR', async () => { - const html = await renderViaHTTP(appPort, '/p1/p2/predefined-ssg/one-level') - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe('{"rest":["one-level"]}') + const data = await renderViaHTTP( + appPort, + `/_next/data/${buildId}/p1/p2/predefined-ssg/one-level.json` + ) + expect(JSON.parse(data).pageProps.params).toEqual({ rest: ['one-level'] }) }) it('[predefined ssg: prerendered catch all] should pass params in getInitialProps during SSR', async () => { - const html = await renderViaHTTP( + const data = await renderViaHTTP( appPort, - '/p1/p2/predefined-ssg/1st-level/2nd-level' - ) - const $ = cheerio.load(html) - expect($('#all-ssg-content').text()).toBe( - '{"rest":["1st-level","2nd-level"]}' + `/_next/data/${buildId}/p1/p2/predefined-ssg/1st-level/2nd-level.json` ) + expect(JSON.parse(data).pageProps.params).toEqual({ + rest: ['1st-level', '2nd-level'], + }) }) it('[ssg: catch-all] should pass params in getStaticProps during client navigation (single)', async () => { @@ -492,6 +502,7 @@ describe('Dynamic Routing', () => { beforeAll(async () => { appPort = await findPort() app = await launchApp(appDir, appPort) + buildId = 'development' }) afterAll(() => killApp(app)) From 4dc3f7ff1f08bb883b3c2f71982edaaddb6d2bd4 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 5 Feb 2020 13:42:28 -0600 Subject: [PATCH 05/17] Add racing to decide between rendering fallback and data --- .../next/next-server/lib/router/router.ts | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 0925cd5643dc9..5fed7ef1b7551 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -409,27 +409,43 @@ export default class Router implements BaseRouter { !(routeInfo.Component as any).getInitialProps } - this.set(route, pathname, query, as, routeInfo) + const doRouteChange = (routeInfo: RouteInfo, emit: boolean) => { + this.set(route, pathname, query, as, routeInfo) - if (error) { - Router.events.emit('routeChangeError', error, as) - throw error - } + if (emit) { + if (error) { + Router.events.emit('routeChangeError', error, as) + throw error + } - Router.events.emit('routeChangeComplete', as) + Router.events.emit('routeChangeComplete', as) + resolve(true) + } + } if ((routeInfo as any).dataRes) { - return ((routeInfo as any).dataRes as Promise).then( - routeInfo => { - this.set(route, pathname, query, as, routeInfo) - // TODO: should we resolve when the fallback is rendered - // or when the data is returned and we render with data? - resolve(true) - }, - reject - ) + const dataRes = (routeInfo as any).dataRes as Promise + // race loading state timeout with data response + // if loading state wins we leave it for 50 ms even if data + // is 1ms after loading state timeout + + Promise.race([ + new Promise(resolve => setTimeout(() => resolve(false), 110)), + dataRes, + ]) + .then((data: any) => { + if (!data) { + // data didn't win the race, show fallback + doRouteChange(routeInfo, false) + } + return dataRes + }) + .then(finalData => { + // render with the data and complete route change + doRouteChange(finalData as RouteInfo, true) + }, reject) } else { - return resolve(true) + doRouteChange(routeInfo, true) } }, reject) }) From 778db98074f8f2a61a892ea4a352c02130a30d52 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 5 Feb 2020 14:13:29 -0600 Subject: [PATCH 06/17] Update size-limit test --- test/integration/size-limit/test/index.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js index f1d7c0af660eb..912e5a6546fb5 100644 --- a/test/integration/size-limit/test/index.test.js +++ b/test/integration/size-limit/test/index.test.js @@ -101,7 +101,7 @@ describe('Production response size', () => { ) // These numbers are without gzip compression! - const delta = responseSizeKilobytes - 195 + const delta = responseSizeKilobytes - 196 expect(delta).toBeLessThanOrEqual(0) // don't increase size expect(delta).toBeGreaterThanOrEqual(-1) // don't decrease size without updating target }) From 591b4a0077280ba2d1eebb37ec3bea57ea41b48a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 5 Feb 2020 14:37:23 -0600 Subject: [PATCH 07/17] Update comment --- packages/next/next-server/lib/router/router.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 5fed7ef1b7551..3ad109ea67be1 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -426,8 +426,6 @@ export default class Router implements BaseRouter { if ((routeInfo as any).dataRes) { const dataRes = (routeInfo as any).dataRes as Promise // race loading state timeout with data response - // if loading state wins we leave it for 50 ms even if data - // is 1ms after loading state timeout Promise.race([ new Promise(resolve => setTimeout(() => resolve(false), 110)), From efd508b914c21c160fcbeb803f9794ad638b1c2e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 5 Feb 2020 14:52:04 -0600 Subject: [PATCH 08/17] Make sure to follow correct route change order --- .../next/next-server/lib/router/router.ts | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 3ad109ea67be1..6e139629e7598 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -393,23 +393,25 @@ export default class Router implements BaseRouter { // If shallow is true and the route exists in the router cache we reuse the previous result this.getRouteInfo(route, pathname, query, as, shallow).then(routeInfo => { - const { error } = routeInfo + const doRouteChange = (routeInfo: RouteInfo, emit: boolean) => { + const { error } = routeInfo - if (error && error.cancelled) { - return resolve(false) - } + if (error && error.cancelled) { + return resolve(false) + } - Router.events.emit('beforeHistoryChange', as) - this.changeState(method, url, addBasePath(as), options) + if (emit) { + Router.events.emit('beforeHistoryChange', as) + this.changeState(method, url, addBasePath(as), options) - if (process.env.NODE_ENV !== 'production') { - const appComp: any = this.components['/_app'].Component - ;(window as any).next.isPrerendered = - appComp.getInitialProps === appComp.origGetInitialProps && - !(routeInfo.Component as any).getInitialProps - } + if (process.env.NODE_ENV !== 'production') { + const appComp: any = this.components['/_app'].Component + ;(window as any).next.isPrerendered = + appComp.getInitialProps === appComp.origGetInitialProps && + !(routeInfo.Component as any).getInitialProps + } + } - const doRouteChange = (routeInfo: RouteInfo, emit: boolean) => { this.set(route, pathname, query, as, routeInfo) if (emit) { From 70546aa985cd0bbe4a3ed2a475df2173209a9268 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Wed, 5 Feb 2020 15:56:55 -0600 Subject: [PATCH 09/17] Make comment more verbose for racing --- packages/next/next-server/lib/router/router.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 6e139629e7598..9cab77014ba80 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -427,8 +427,10 @@ export default class Router implements BaseRouter { if ((routeInfo as any).dataRes) { const dataRes = (routeInfo as any).dataRes as Promise - // race loading state timeout with data response + // to prevent a flash of the fallback page we delay showing it for + // 110ms and race the timeout with the data response. If the data + // beats the timeout we skip showing the fallback Promise.race([ new Promise(resolve => setTimeout(() => resolve(false), 110)), dataRes, From d56c58361c3c5477f1047d0b9486b352a7269bc2 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 20:09:52 -0600 Subject: [PATCH 10/17] Revert getStaticData to only return Promise --- .../next/next-server/lib/router/router.ts | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 9cab77014ba80..5fbf02370ab02 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -526,25 +526,18 @@ export default class Router implements BaseRouter { return routeInfo } - // if we have data in cache resolve with the data - // if not we we resolve with fallback routeInfo - + // resolve with fallback routeInfo and promise for data if (isSSG || isSSP) { - const dataRes = isSSG - ? this._getStaticData(as) - : this._getServerData(as) - - return Promise.resolve( - typeof dataRes.then !== 'function' - ? handleData(dataRes) - : { - ...routeInfo, - props: {}, - dataRes: this._getData(() => - dataRes.then((props: any) => handleData(props)) - ), - } - ) + return Promise.resolve({ + ...routeInfo, + props: {}, + dataRes: this._getData(() => + (isSSG + ? this._getStaticData(as) + : this._getServerData(as) + ).then((props: any) => handleData(props)) + ), + }) } return this._getData(() => @@ -755,11 +748,11 @@ export default class Router implements BaseRouter { }) } - _getStaticData = (asPath: string): Promise | any => { + _getStaticData = (asPath: string): Promise => { const pathname = prepareRoute(parse(asPath).pathname!) return process.env.NODE_ENV === 'production' && this.sdc[pathname] - ? this.sdc[pathname] + ? Promise.resolve(this.sdc[pathname]) : fetchNextData(pathname, null, data => (this.sdc[pathname] = data)) } From cebfcbcb887a6f8d9dcd243810f5ef4a1ea4784a Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 20:23:54 -0600 Subject: [PATCH 11/17] Make sure to update URL on fallback --- packages/next/next-server/lib/router/router.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index 5fbf02370ab02..a90ef05a8ac2d 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -400,16 +400,14 @@ export default class Router implements BaseRouter { return resolve(false) } - if (emit) { - Router.events.emit('beforeHistoryChange', as) - this.changeState(method, url, addBasePath(as), options) - - if (process.env.NODE_ENV !== 'production') { - const appComp: any = this.components['/_app'].Component - ;(window as any).next.isPrerendered = - appComp.getInitialProps === appComp.origGetInitialProps && - !(routeInfo.Component as any).getInitialProps - } + Router.events.emit('beforeHistoryChange', as) + this.changeState(method, url, addBasePath(as), options) + + if (process.env.NODE_ENV !== 'production') { + const appComp: any = this.components['/_app'].Component + ;(window as any).next.isPrerendered = + appComp.getInitialProps === appComp.origGetInitialProps && + !(routeInfo.Component as any).getInitialProps } this.set(route, pathname, query, as, routeInfo) From 66e7e511a0dccc1c796a599032925c22047bfabc Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 20:53:53 -0600 Subject: [PATCH 12/17] Add retrying for data, de-dupe initial fallback request, and merge fallback replace --- packages/next/client/index.js | 82 ++++++++----------- .../next/next-server/lib/router/router.ts | 40 ++++++--- 2 files changed, 63 insertions(+), 59 deletions(-) diff --git a/packages/next/client/index.js b/packages/next/client/index.js index 8f5618f770b7e..4a802ec39c98c 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -44,8 +44,11 @@ const { assetPrefix, runtimeConfig, dynamicIds, + isFallback, } = data +let fallbackDataPromise = Promise.resolve() + const prefix = assetPrefix || '' // With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time @@ -96,51 +99,38 @@ class Container extends React.Component { }) } - // call router.replace to trigger data fetching while - // the fallback is shown - - if (data.isFallback) { - router.replace( - { - pathname: page, - query: { - ...router.query, - ...parseQs(location.search.substr(1)), - }, - }, - asPath, - { _h: 1 } - ) - } - - // If page was exported and has a querystring - // If it's a dynamic route or has a querystring - - if ( - !data.isFallback && - router.isSsr && - ((data.nextExport && - (isDynamicRoute(router.pathname) || location.search)) || - (Component && Component.__N_SSG && location.search)) - ) { - // update query on mount for exported pages - router.replace( - router.pathname + - '?' + - stringifyQs({ - ...router.query, - ...parseQs(location.search.substr(1)), - }), - asPath, - { - // WARNING: `_h` is an internal option for handing Next.js - // client-side hydration. Your app should _never_ use this property. - // It may change at any time without notice. - _h: 1, - shallow: true, - } - ) - } + // wait for fallbackDataPromise so we don't kick off an extra + // request if one is pending + fallbackDataPromise.then(() => { + // If page was exported and has a querystring + // If it's a dynamic route or has a querystring + // if it's a fallback page + if ( + router.isSsr && + (isFallback || + (data.nextExport && + (isDynamicRoute(router.pathname) || location.search)) || + (Component && Component.__N_SSG && location.search)) + ) { + // update query on mount for exported pages + router.replace( + router.pathname + + '?' + + stringifyQs({ + ...router.query, + ...parseQs(location.search.substr(1)), + }), + asPath, + { + // WARNING: `_h` is an internal option for handing Next.js + // client-side hydration. Your app should _never_ use this property. + // It may change at any time without notice. + _h: 1, + shallow: !isFallback, + } + ) + } + }) if (process.env.__NEXT_TEST_MODE) { window.__NEXT_HYDRATED = true @@ -240,7 +230,7 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { // kick off static data request now so it's in the cache // when we re-render post-hydration if (data.isFallback) { - router._getStaticData(asPath).catch(() => {}) + fallbackDataPromise = router._getStaticData(asPath).catch(() => {}) } render(renderCtx) diff --git a/packages/next/next-server/lib/router/router.ts b/packages/next/next-server/lib/router/router.ts index a90ef05a8ac2d..d658f49836270 100644 --- a/packages/next/next-server/lib/router/router.ts +++ b/packages/next/next-server/lib/router/router.ts @@ -78,17 +78,15 @@ const fetchNextData = ( ) .then(res => { if (!res.ok) { - throw new Error(`Failed to load static props`) + const error = new Error(`Failed to load static props`) + ;(error as any).statusCode = res.status + throw error } return res.json() }) .then(data => { return cb ? cb(data) : data }) - .catch((err: Error) => { - ;(err as any).code = 'PAGE_LOAD_ERROR' - throw err - }) } export default class Router implements BaseRouter { @@ -393,15 +391,20 @@ export default class Router implements BaseRouter { // If shallow is true and the route exists in the router cache we reuse the previous result this.getRouteInfo(route, pathname, query, as, shallow).then(routeInfo => { - const doRouteChange = (routeInfo: RouteInfo, emit: boolean) => { + let emitHistory = false + + const doRouteChange = (routeInfo: RouteInfo, complete: boolean) => { const { error } = routeInfo if (error && error.cancelled) { return resolve(false) } - Router.events.emit('beforeHistoryChange', as) - this.changeState(method, url, addBasePath(as), options) + if (!emitHistory) { + emitHistory = true + Router.events.emit('beforeHistoryChange', as) + this.changeState(method, url, addBasePath(as), options) + } if (process.env.NODE_ENV !== 'production') { const appComp: any = this.components['/_app'].Component @@ -412,7 +415,7 @@ export default class Router implements BaseRouter { this.set(route, pathname, query, as, routeInfo) - if (emit) { + if (complete) { if (error) { Router.events.emit('routeChangeError', error, as) throw error @@ -526,14 +529,25 @@ export default class Router implements BaseRouter { // resolve with fallback routeInfo and promise for data if (isSSG || isSSP) { + const dataMethod = () => + isSSG ? this._getStaticData(as) : this._getServerData(as) + + const retry = (error: Error & { statusCode: number }) => { + if (error.statusCode === 404) { + throw error + } + return dataMethod() + } + return Promise.resolve({ ...routeInfo, props: {}, dataRes: this._getData(() => - (isSSG - ? this._getStaticData(as) - : this._getServerData(as) - ).then((props: any) => handleData(props)) + dataMethod() + // we retry for data twice unless we get a 404 + .catch(retry) + .catch(retry) + .then((props: any) => handleData(props)) ), }) } From e5db1bbe7ed8a25dbef9a1383434504f0208e0e7 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 21:02:06 -0600 Subject: [PATCH 13/17] Update to add preload for fallback pages data --- packages/next/client/index.js | 68 +++++++++++++------------------ packages/next/pages/_document.tsx | 13 +++++- 2 files changed, 40 insertions(+), 41 deletions(-) diff --git a/packages/next/client/index.js b/packages/next/client/index.js index 4a802ec39c98c..9bf4edbded0a1 100644 --- a/packages/next/client/index.js +++ b/packages/next/client/index.js @@ -47,8 +47,6 @@ const { isFallback, } = data -let fallbackDataPromise = Promise.resolve() - const prefix = assetPrefix || '' // With dynamic assetPrefix it's no longer possible to set assetPrefix at the build time @@ -99,38 +97,34 @@ class Container extends React.Component { }) } - // wait for fallbackDataPromise so we don't kick off an extra - // request if one is pending - fallbackDataPromise.then(() => { - // If page was exported and has a querystring - // If it's a dynamic route or has a querystring - // if it's a fallback page - if ( - router.isSsr && - (isFallback || - (data.nextExport && - (isDynamicRoute(router.pathname) || location.search)) || - (Component && Component.__N_SSG && location.search)) - ) { - // update query on mount for exported pages - router.replace( - router.pathname + - '?' + - stringifyQs({ - ...router.query, - ...parseQs(location.search.substr(1)), - }), - asPath, - { - // WARNING: `_h` is an internal option for handing Next.js - // client-side hydration. Your app should _never_ use this property. - // It may change at any time without notice. - _h: 1, - shallow: !isFallback, - } - ) - } - }) + // If page was exported and has a querystring + // If it's a dynamic route or has a querystring + // if it's a fallback page + if ( + router.isSsr && + (isFallback || + (data.nextExport && + (isDynamicRoute(router.pathname) || location.search)) || + (Component && Component.__N_SSG && location.search)) + ) { + // update query on mount for exported pages + router.replace( + router.pathname + + '?' + + stringifyQs({ + ...router.query, + ...parseQs(location.search.substr(1)), + }), + asPath, + { + // WARNING: `_h` is an internal option for handing Next.js + // client-side hydration. Your app should _never_ use this property. + // It may change at any time without notice. + _h: 1, + shallow: !isFallback, + } + ) + } if (process.env.__NEXT_TEST_MODE) { window.__NEXT_HYDRATED = true @@ -227,12 +221,6 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => { const renderCtx = { App, Component, props, err: initialErr } if (process.env.NODE_ENV === 'production') { - // kick off static data request now so it's in the cache - // when we re-render post-hydration - if (data.isFallback) { - fallbackDataPromise = router._getStaticData(asPath).catch(() => {}) - } - render(renderCtx) return emitter } diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 5d63deda249b8..8c67ffd35ed39 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -296,7 +296,7 @@ export class Head extends Component< headTags, } = this.context._documentProps const { _devOnlyInvalidateCacheQueryString } = this.context - const { page, buildId } = __NEXT_DATA__ + const { page, buildId, isFallback } = __NEXT_DATA__ let { head } = this.context._documentProps let children = this.props.children @@ -479,6 +479,17 @@ export class Head extends Component< href={canonicalBase + getAmpPath(ampPath, dangerousAsPath)} /> )} + {isFallback && ( + + )} {this.getCssLinks()} {page !== '/_error' && ( Date: Thu, 6 Feb 2020 21:36:56 -0600 Subject: [PATCH 14/17] Add test for data preload link --- test/integration/prerender/test/index.test.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index a1bad06c53b7c..8a3014c483e3b 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -255,13 +255,16 @@ const runTests = (dev = false) => { const html = await renderViaHTTP(appPort, '/blog/post-1') if (dev) { - expect( - JSON.parse( - cheerio - .load(html)('#__NEXT_DATA__') - .text() - ).isFallback - ).toBe(true) + const $ = cheerio.load(html) + expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(true) + + const preloadLink = Array.from($('link[rel=preload]')).find(el => + el.attribs.href.endsWith('post-1.json') + ) + + expect(preloadLink.attribs.href).toBe( + `/_next/data/${buildId}/blog/post-1.json` + ) } else { expect(html).toMatch(/Post:.*?post-1/) } From ff66599716fb6256a5ae69b95dc1e5d278c6a43d Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 23:32:07 -0600 Subject: [PATCH 15/17] Use pre-built fallback in production mode --- packages/next/next-server/server/next-server.ts | 10 +++++++++- packages/next/next-server/server/spr-cache.ts | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index f7495fb01e18d..382237eb0dc42 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -43,7 +43,12 @@ import Router, { } from './router' import { sendHTML } from './send-html' import { serveStatic } from './serve-static' -import { getSprCache, initializeSprCache, setSprCache } from './spr-cache' +import { + getSprCache, + initializeSprCache, + setSprCache, + getFallback, +} from './spr-cache' import { isBlockedPage } from './utils' import { Redirect, @@ -1012,6 +1017,9 @@ export default class Server { // render fallback if cached data wasn't available if (!isResSent(res) && !isDataReq && isDynamicRoute(pathname)) { + if (!this.renderOpts.dev) { + return getFallback(pathname) + } query.__nextFallback = 'true' let html = '' if (isLikeServerless) { diff --git a/packages/next/next-server/server/spr-cache.ts b/packages/next/next-server/server/spr-cache.ts index 0de1a419e6049..36ccae2d4fc0e 100644 --- a/packages/next/next-server/server/spr-cache.ts +++ b/packages/next/next-server/server/spr-cache.ts @@ -92,6 +92,11 @@ export function initializeSprCache({ }) } +export async function getFallback(page: string): Promise { + page = normalizePagePath(page) + return readFile(getSeedPath(page, 'html'), 'utf8') +} + // get data from SPR cache if available export async function getSprCache( pathname: string From 6f50f3d257d60dce329a872d67a38e3ee3f2e71e Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Thu, 6 Feb 2020 23:33:41 -0600 Subject: [PATCH 16/17] Remove preload link for fallback from _document --- packages/next/pages/_document.tsx | 13 +------------ test/integration/prerender/test/index.test.js | 8 -------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/packages/next/pages/_document.tsx b/packages/next/pages/_document.tsx index 8c67ffd35ed39..5d63deda249b8 100644 --- a/packages/next/pages/_document.tsx +++ b/packages/next/pages/_document.tsx @@ -296,7 +296,7 @@ export class Head extends Component< headTags, } = this.context._documentProps const { _devOnlyInvalidateCacheQueryString } = this.context - const { page, buildId, isFallback } = __NEXT_DATA__ + const { page, buildId } = __NEXT_DATA__ let { head } = this.context._documentProps let children = this.props.children @@ -479,17 +479,6 @@ export class Head extends Component< href={canonicalBase + getAmpPath(ampPath, dangerousAsPath)} /> )} - {isFallback && ( - - )} {this.getCssLinks()} {page !== '/_error' && ( { if (dev) { const $ = cheerio.load(html) expect(JSON.parse($('#__NEXT_DATA__').text()).isFallback).toBe(true) - - const preloadLink = Array.from($('link[rel=preload]')).find(el => - el.attribs.href.endsWith('post-1.json') - ) - - expect(preloadLink.attribs.href).toBe( - `/_next/data/${buildId}/blog/post-1.json` - ) } else { expect(html).toMatch(/Post:.*?post-1/) } From e64743485437fb623cd7e2eaa9f73a2352730321 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 7 Feb 2020 02:09:59 -0600 Subject: [PATCH 17/17] Update to make sure fallback is rendered correctly for serverless --- .../webpack/loaders/next-serverless-loader.ts | 4 +++- .../next/next-server/server/next-server.ts | 24 ++++++++++--------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 8f6d29349837b..3f322cb322d6a 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -308,7 +308,9 @@ const nextServerlessLoader: loader.Loader = function() { // if provided from worker or params if we're parsing them here renderOpts.params = _params || params - let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params), renderOpts) + const isFallback = parsedUrl.query.__nextFallback + + let result = await renderToHTML(req, res, "${page}", Object.assign({}, unstable_getStaticProps ? {} : parsedUrl.query, nowParams ? nowParams : params, _params, isFallback ? { __nextFallback: 'true' } : {}), renderOpts) if (_nextData && !fromExport) { const payload = JSON.stringify(renderOpts.pageData) diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 382237eb0dc42..8d43b76ceb884 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -1017,19 +1017,21 @@ export default class Server { // render fallback if cached data wasn't available if (!isResSent(res) && !isDataReq && isDynamicRoute(pathname)) { - if (!this.renderOpts.dev) { - return getFallback(pathname) - } - query.__nextFallback = 'true' let html = '' - if (isLikeServerless) { - this.prepareServerlessUrl(req, query) - html = await (result.Component as any).renderReqToHTML(req, res) + + if (!this.renderOpts.dev) { + html = await getFallback(pathname) } else { - html = (await renderToHTML(req, res, pathname, query, { - ...result, - ...opts, - })) as string + query.__nextFallback = 'true' + if (isLikeServerless) { + this.prepareServerlessUrl(req, query) + html = await (result.Component as any).renderReqToHTML(req, res) + } else { + html = (await renderToHTML(req, res, pathname, query, { + ...result, + ...opts, + })) as string + } } this.__sendPayload(res, html, 'text/html; charset=utf-8')