From 61fe286fff6725ca11c3f4dfbf5d258ba42f7f7a Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 16 Aug 2022 17:31:24 +0100 Subject: [PATCH 01/22] fix: bypass handler function for non-prerendered dynamic routes with fallback: false --- packages/runtime/src/helpers/redirects.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index 67090f1d3f..893d648f4d 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -195,9 +195,10 @@ const generateDynamicRewrites = ({ return } if (route.page in prerenderedDynamicRoutes) { + const { fallback } = prerenderedDynamicRoutes[route.page] if (matchesMiddleware(middleware, route.page)) { dynamicRoutesThatMatchMiddleware.push(route.page) - } else { + } else if (fallback !== false) { dynamicRewrites.push( ...redirectsForNextRoute({ buildId, route: route.page, basePath, to: ODB_FUNCTION_PATH, status: 200, i18n }), ) From b7a68728bb6acf6c125549688142a100de0789db Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 16 Aug 2022 17:32:37 +0100 Subject: [PATCH 02/22] fix: remove catch all handler redirect to support fallback false and custom 404s --- packages/runtime/src/helpers/redirects.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index 893d648f4d..3a87c7b176 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -295,13 +295,6 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...dynamicRewrites) routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware) - // Final fallback - netlifyConfig.redirects.push({ - from: `${basePath}/*`, - to: HANDLER_FUNCTION_PATH, - status: 200, - }) - const middlewareMatches = new Set(routesThatMatchMiddleware).size if (middlewareMatches > 0) { console.log( From b8e19501334b3fdfcb35a18ddb2a8cb75c20f01a Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 16 Aug 2022 17:33:01 +0100 Subject: [PATCH 03/22] feat: support custom 404 pages in all locales --- packages/runtime/src/helpers/redirects.ts | 29 +++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index 3a87c7b176..a894415294 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -56,6 +56,33 @@ const generateLocaleRedirects = ({ return redirects } +const generateCustom404Redirects = ({ i18n }: Pick): NetlifyConfig['redirects'] => { + const redirects: NetlifyConfig['redirects'] = [] + + if (i18n) { + i18n.locales.forEach((locale) => { + redirects.push({ + from: `/${locale}/*`, + to: `/server/pages/${locale}/404.html`, + status: 404, + }) + }) + redirects.push({ + from: '/*', + to: `/server/pages/${i18n.defaultLocale}/404.html`, + status: 404, + }) + } else { + redirects.push({ + from: '/*', + to: '/server/pages/404.html', + status: 404, + }) + } + + return redirects +} + export const generateStaticRedirects = ({ netlifyConfig, nextConfig: { i18n, basePath }, @@ -245,6 +272,8 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash })) } + netlifyConfig.redirects.push(...generateCustom404Redirects({ i18n })) + // This is only used in prod, so dev uses `next dev` directly netlifyConfig.redirects.push( // API routes always need to be served from the regular function From 0996adc58e5c199b6fc614ece677edd32a869154 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 31 Aug 2022 11:34:18 +0100 Subject: [PATCH 04/22] feat: add custom 404 handling for static routes --- packages/runtime/src/helpers/redirects.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index a894415294..911ddd5abc 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -56,26 +56,29 @@ const generateLocaleRedirects = ({ return redirects } -const generateCustom404Redirects = ({ i18n }: Pick): NetlifyConfig['redirects'] => { +const generate404Redirects = ({ + i18n, + basePath, +}: Pick): NetlifyConfig['redirects'] => { const redirects: NetlifyConfig['redirects'] = [] if (i18n) { i18n.locales.forEach((locale) => { redirects.push({ - from: `/${locale}/*`, - to: `/server/pages/${locale}/404.html`, + from: `${basePath}/${locale}/*`, + to: `${basePath}/server/pages/${locale}/404.html`, status: 404, }) }) redirects.push({ - from: '/*', - to: `/server/pages/${i18n.defaultLocale}/404.html`, + from: `${basePath}/*`, + to: `${basePath}/server/pages/${i18n.defaultLocale}/404.html`, status: 404, }) } else { redirects.push({ - from: '/*', - to: '/server/pages/404.html', + from: `${basePath}/*`, + to: `${basePath}/server/pages/404.html`, status: 404, }) } @@ -324,6 +327,8 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...dynamicRewrites) routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware) + netlifyConfig.redirects.push(...generate404Redirects({ i18n, basePath })) + const middlewareMatches = new Set(routesThatMatchMiddleware).size if (middlewareMatches > 0) { console.log( From 52ee02cc1b2b292d869c6e98a06cf62c03ef273c Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 31 Aug 2022 11:35:15 +0100 Subject: [PATCH 05/22] chore: refactor hidden path redirects --- packages/runtime/src/helpers/redirects.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index 911ddd5abc..ff9dff1340 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -23,6 +23,14 @@ import { const matchesMiddleware = (middleware: Array, route: string): boolean => middleware.some((middlewarePath) => route.startsWith(middlewarePath)) +const generateHiddenPathRedirects = ({ basePath }: Pick): NetlifyConfig['redirects'] => + HIDDEN_PATHS.map((path) => ({ + from: `${basePath}${path}`, + to: '/404.html', + status: 404, + force: true, + })) + const generateLocaleRedirects = ({ i18n, basePath, @@ -262,21 +270,12 @@ export const generateRedirects = async ({ join(netlifyConfig.build.publish, 'routes-manifest.json'), ) - netlifyConfig.redirects.push( - ...HIDDEN_PATHS.map((path) => ({ - from: `${basePath}${path}`, - to: '/404.html', - status: 404, - force: true, - })), - ) + netlifyConfig.redirects.push(...generateHiddenPathRedirects({ basePath })) if (i18n && i18n.localeDetection !== false) { netlifyConfig.redirects.push(...generateLocaleRedirects({ i18n, basePath, trailingSlash })) } - netlifyConfig.redirects.push(...generateCustom404Redirects({ i18n })) - // This is only used in prod, so dev uses `next dev` directly netlifyConfig.redirects.push( // API routes always need to be served from the regular function From 18c233330087b90713bd73ba242c48d957987005 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Fri, 2 Sep 2022 13:55:42 +0100 Subject: [PATCH 06/22] feat: add support for isr 404 pages --- packages/runtime/src/helpers/redirects.ts | 33 ++++++++++++++++++----- packages/runtime/src/helpers/utils.ts | 3 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index ff9dff1340..58925b0175 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -14,6 +14,7 @@ import { RoutesManifest } from './types' import { getApiRewrites, getPreviewRewrites, + is404Route, isApiRoute, redirectsForNextRoute, redirectsForNextRouteWithData, @@ -65,12 +66,30 @@ const generateLocaleRedirects = ({ } const generate404Redirects = ({ - i18n, + staticRouteEntries, basePath, -}: Pick): NetlifyConfig['redirects'] => { + i18n, + buildId, +}: { + staticRouteEntries: Array<[string, SsgRoute]> + basePath: string + i18n: NextConfig['i18n'] + buildId: string +}): NetlifyConfig['redirects'] => { const redirects: NetlifyConfig['redirects'] = [] - if (i18n) { + const isIsr404 = staticRouteEntries.some( + ([route, { initialRevalidateSeconds }]) => is404Route(route, i18n) && initialRevalidateSeconds !== false, + ) + + if (isIsr404) { + redirects.push({ + from: `${basePath}/*`, + to: ODB_FUNCTION_PATH, + status: 404, + }, + ) + } else if (i18n?.locales?.length) { i18n.locales.forEach((locale) => { redirects.push({ from: `${basePath}/${locale}/*`, @@ -161,7 +180,7 @@ const generateStaticIsrRewrites = ({ const staticRoutePaths = new Set() const staticIsrRewrites: NetlifyConfig['redirects'] = [] staticRouteEntries.forEach(([route, { initialRevalidateSeconds }]) => { - if (isApiRoute(route)) { + if (isApiRoute(route) || is404Route(route, i18n)) { return } staticRoutePaths.add(route) @@ -229,7 +248,7 @@ const generateDynamicRewrites = ({ const dynamicRewrites: NetlifyConfig['redirects'] = [] const dynamicRoutesThatMatchMiddleware: Array = [] dynamicRoutes.forEach((route) => { - if (isApiRoute(route.page)) { + if (isApiRoute(route.page) || is404Route(route.page, i18n)) { return } if (route.page in prerenderedDynamicRoutes) { @@ -306,7 +325,7 @@ export const generateRedirects = async ({ // Add rewrites for all static SSR routes. This is Next 12+ staticRoutes?.forEach((route) => { - if (staticRoutePaths.has(route.page) || isApiRoute(route.page)) { + if (staticRoutePaths.has(route.page) || isApiRoute(route.page) || is404Route(route.page)) { // Prerendered static routes are either handled by the CDN or are ISR return } @@ -326,7 +345,7 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...dynamicRewrites) routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware) - netlifyConfig.redirects.push(...generate404Redirects({ i18n, basePath })) + netlifyConfig.redirects.push(...generate404Redirects({ staticRouteEntries, basePath, i18n, buildId })) const middlewareMatches = new Set(routesThatMatchMiddleware).size if (middlewareMatches > 0) { diff --git a/packages/runtime/src/helpers/utils.ts b/packages/runtime/src/helpers/utils.ts index 08cc0a160b..d6f196fa6d 100644 --- a/packages/runtime/src/helpers/utils.ts +++ b/packages/runtime/src/helpers/utils.ts @@ -73,6 +73,9 @@ const netlifyRoutesForNextRoute = (route: string, buildId: string, i18n?: I18n): export const isApiRoute = (route: string) => route.startsWith('/api/') || route === '/api' +export const is404Route = (route: string, i18n?: I18n) => + i18n ? i18n.locales.some((locale) => route === `/${locale}/404`) : route === '/404' + export const redirectsForNextRoute = ({ route, buildId, From 8a361aec2adc9afdd951019377759d0567f49654 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Fri, 2 Sep 2022 13:56:39 +0100 Subject: [PATCH 07/22] feat: swap cache patch for forced manual revalidate --- packages/runtime/src/helpers/files.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index f1761fdcee..7c9fd953c6 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -334,7 +334,9 @@ const getServerFile = (root: string, includeBase = true) => { } const baseServerReplacements: Array<[string, string]> = [ - [`let ssgCacheKey = `, `let ssgCacheKey = process.env._BYPASS_SSG || `], + [`checkIsManualRevalidate(req, this.renderOpts.previewProps)`, `checkIsManualRevalidate({ headers: null }, null)`], + [`isManualRevalidate && (fallbackMode !== false || hadCache)`, `isManualRevalidate && hadCache`], + [`private: isPreviewMode || is404Page && cachedData`, `private: isPreviewMode && cachedData`], ] const nextServerReplacements: Array<[string, string]> = [ From 2073b271957f08d8a9ba2000be322badb020047c Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Fri, 2 Sep 2022 14:38:47 +0100 Subject: [PATCH 08/22] chore: add docs for patches --- packages/runtime/src/helpers/files.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 7c9fd953c6..2e7601a09f 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -333,6 +333,9 @@ const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } +// force manual revalidation so the ODB handler always receives fresh content +// ensure fallback behaviour is bypassed for pre-rendered pages +// ensure ISR 404 pages send the correct SWR cache headers const baseServerReplacements: Array<[string, string]> = [ [`checkIsManualRevalidate(req, this.renderOpts.previewProps)`, `checkIsManualRevalidate({ headers: null }, null)`], [`isManualRevalidate && (fallbackMode !== false || hadCache)`, `isManualRevalidate && hadCache`], From 046d375722bc380dbc01d1e58f3183d1440b519d Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 12:10:15 +0100 Subject: [PATCH 09/22] feat: use revalidate header instead of server patch --- packages/runtime/src/helpers/files.ts | 2 -- packages/runtime/src/templates/getHandler.ts | 23 +++++++++++++++----- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 2e7601a09f..6654bb2916 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -333,11 +333,9 @@ const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } -// force manual revalidation so the ODB handler always receives fresh content // ensure fallback behaviour is bypassed for pre-rendered pages // ensure ISR 404 pages send the correct SWR cache headers const baseServerReplacements: Array<[string, string]> = [ - [`checkIsManualRevalidate(req, this.renderOpts.previewProps)`, `checkIsManualRevalidate({ headers: null }, null)`], [`isManualRevalidate && (fallbackMode !== false || hadCache)`, `isManualRevalidate && hadCache`], [`private: isPreviewMode || is404Page && cachedData`, `private: isPreviewMode && cachedData`], ] diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index c135dc2c0b..1ad3434871 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -1,6 +1,7 @@ import { HandlerContext, HandlerEvent } from '@netlify/functions' // Aliasing like this means the editor may be able to syntax-highlight the string import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge' +import type { PrerenderManifest } from 'next/dist/build' import { outdent as javascript } from 'outdent' import type { NextConfig } from '../helpers/config' @@ -25,8 +26,15 @@ type Mutable = { } // We return a function and then call `toString()` on it to serialise it as the launcher function -// eslint-disable-next-line max-params -const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => { +const makeHandler = ( + conf: NextConfig, + app, + pageRoot, + prerenderManifest: PrerenderManifest, + staticManifest: Array<[string, string]> = [], + mode = 'ssr', + // eslint-disable-next-line max-params +) => { // Change working directory into the site root, unless using Nx, which moves the // dist directory and handles this itself const dir = path.resolve(__dirname, app) @@ -81,6 +89,10 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str }) const requestHandler = nextServer.getRequestHandler() const server = new Server(async (req, res) => { + if (prerenderManifest.preview) { + // force manual revalidation for fresh content + req.headers['x-prerender-revalidate'] = prerenderManifest.preview.previewModeId + } try { await requestHandler(req, res) } catch (error) { @@ -157,15 +169,16 @@ export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDi const { builder } = require("@netlify/functions"); const { config } = require("${publishDir}/required-server-files.json") - let staticManifest + let prerenderManifest, staticManifest try { + prerenderManifest = require("${publishDir}/prerender-manifest.json") staticManifest = require("${publishDir}/static-manifest.json") } catch {} const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages")); exports.handler = ${ isODB - ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));` - : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');` + ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, prerenderManifest, staticManifest, 'odb'));` + : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, prerenderManifest, staticManifest, 'ssr');` } ` From 69a41462951c93f59953fc99ce0d0aeb7168f5d7 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 12:11:05 +0100 Subject: [PATCH 10/22] test: add dynamic routing and fallback tests --- .../default/dynamic-routes.spec.ts | 127 +++++++++++++++++- 1 file changed, 122 insertions(+), 5 deletions(-) diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index a0cf095942..cb86c8c8f5 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -1,7 +1,124 @@ +describe('Static Routing', () => { + it('loads show #42 via SSR on a static route', () => { + cy.request('/getServerSideProps/static/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.body).to.contain('Sleepy Hollow') + }) + }) + it('loads show #71 from a static file on a static route', () => { + cy.request('/getStaticProps/static/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Dancing with the Stars') + }) + }) + it('loads show #71 via ODB on a static route', () => { + cy.request('/getStaticProps/with-revalidate/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Dancing with the Stars') + }) + }) +}) + describe('Dynamic Routing', () => { - it('loads page', () => { - cy.visit('/shows/250') - cy.findByRole('heading').should('contain', '250') - cy.findByText('Kirby Buckets') + it('loads show #1 via SSR on a dynamic route', () => { + cy.request('/getServerSideProps/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads show #1 via SSR on a dynamic catch-all route', () => { + cy.request('/getServerSideProps/all/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads show #1 from a static file on a prerendered dynamic route with fallback: false', () => { + cy.request('/getStaticProps/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('returns a 404 on a non-prerendered dynamic route with fallback: false', () => { + cy.request({ url: '/getStaticProps/3/', failOnStatusCode: false }).then((res) => { + expect(res.status).to.eq(404) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Custom 404') + }) + }) + it('loads show #1 from a static file on a prerendered dynamic route with fallback: true', () => { + cy.request('/getStaticProps/withFallback/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads a fallback page on a non-prerendered dynamic route with fallback: true', () => { + cy.request('/getStaticProps/withFallback/3/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Loading...') + }) + }) + it('loads show #1 from a static file on a prerendered dynamic route with fallback: blocking', () => { + cy.request('/getStaticProps/withFallbackBlocking/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads show #1 via ODB on a non-prerendered dynamic route with fallback: blocking', () => { + cy.request('/getStaticProps/withFallbackBlocking/3/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'odb') + expect(res.body).to.contain('Bitten') + }) + }) + it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: false', () => { + cy.request('/getStaticProps/withRevalidate/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('returns a 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { + cy.request({ url: '/getStaticProps/withRevalidate/3/', failOnStatusCode: false }).then((res) => { + expect(res.status).to.eq(404) + expect(res.headers).to.not.have.property('x-render-mode') + expect(res.body).to.contain('Custom 404') + }) + }) + it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: true', () => { + cy.request('/getStaticProps/withRevalidate/withFallback/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads a fallback page via ODB on a non-prerendered dynamic route with revalidate and fallback: true', () => { + cy.request('/getStaticProps/withRevalidate/withFallback/3/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Loading...') + }) + }) + it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: blocking', () => { + cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/1/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Under the Dome') + }) + }) + it('loads show #1 via ODB on a non-prerendered dynamic route with revalidate and fallback: blocking', () => { + cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/3/').then((res) => { + expect(res.status).to.eq(200) + expect(res.headers).to.have.property('x-render-mode', 'isr') + expect(res.body).to.contain('Bitten') + }) }) -}) \ No newline at end of file +}) From 2b67d089d6c2a26a6f74c0088e83bf4b9f0c10a5 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 12:14:53 +0100 Subject: [PATCH 11/22] chore: fix eslint error --- packages/runtime/src/helpers/redirects.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index 58925b0175..f860abc806 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -69,12 +69,10 @@ const generate404Redirects = ({ staticRouteEntries, basePath, i18n, - buildId, }: { staticRouteEntries: Array<[string, SsgRoute]> basePath: string i18n: NextConfig['i18n'] - buildId: string }): NetlifyConfig['redirects'] => { const redirects: NetlifyConfig['redirects'] = [] @@ -84,11 +82,10 @@ const generate404Redirects = ({ if (isIsr404) { redirects.push({ - from: `${basePath}/*`, - to: ODB_FUNCTION_PATH, - status: 404, - }, - ) + from: `${basePath}/*`, + to: ODB_FUNCTION_PATH, + status: 404, + }) } else if (i18n?.locales?.length) { i18n.locales.forEach((locale) => { redirects.push({ @@ -345,7 +342,7 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...dynamicRewrites) routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware) - netlifyConfig.redirects.push(...generate404Redirects({ staticRouteEntries, basePath, i18n, buildId })) + netlifyConfig.redirects.push(...generate404Redirects({ staticRouteEntries, basePath, i18n })) const middlewareMatches = new Set(routesThatMatchMiddleware).size if (middlewareMatches > 0) { From 39d6ea9d022205d4f6de95d86b5c570ae1a4c6c9 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 12:29:47 +0100 Subject: [PATCH 12/22] fix: remove fallback: true handling until runtime fix lands --- packages/runtime/src/helpers/files.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 6654bb2916..fe44ea62b7 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -333,10 +333,8 @@ const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } -// ensure fallback behaviour is bypassed for pre-rendered pages // ensure ISR 404 pages send the correct SWR cache headers const baseServerReplacements: Array<[string, string]> = [ - [`isManualRevalidate && (fallbackMode !== false || hadCache)`, `isManualRevalidate && hadCache`], [`private: isPreviewMode || is404Page && cachedData`, `private: isPreviewMode && cachedData`], ] From 3f5e675631d8da2148b63448f54ad59024e6c927 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 12:30:39 +0100 Subject: [PATCH 13/22] test: update tests while fallback:true works like fallback:blocking --- cypress/integration/default/dynamic-routes.spec.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index cb86c8c8f5..663139a7fa 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -61,8 +61,10 @@ describe('Dynamic Routing', () => { it('loads a fallback page on a non-prerendered dynamic route with fallback: true', () => { cy.request('/getStaticProps/withFallback/3/').then((res) => { expect(res.status).to.eq(200) - expect(res.headers).to.have.property('x-render-mode', 'isr') - expect(res.body).to.contain('Loading...') + // expect 'odb' until https://github.com/netlify/pillar-runtime/issues/438 is fixed + expect(res.headers).to.have.property('x-render-mode', 'odb') + // expect 'Bitten' until the above is fixed and we can test for fallback 'Loading...' message + expect(res.body).to.contain('Bitten') }) }) it('loads show #1 from a static file on a prerendered dynamic route with fallback: blocking', () => { @@ -104,7 +106,8 @@ describe('Dynamic Routing', () => { cy.request('/getStaticProps/withRevalidate/withFallback/3/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') - expect(res.body).to.contain('Loading...') + // expect 'Bitten' until https://github.com/netlify/pillar-runtime/issues/438 is fixed + expect(res.body).to.contain('Bitten') }) }) it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: blocking', () => { From 5806400450a17dd61714f1803ad5d1d4583c431b Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 14:56:10 +0100 Subject: [PATCH 14/22] test: fix jest tests --- test/__snapshots__/index.js.snap | 119 ++++--------------------------- test/index.js | 12 ++-- 2 files changed, 21 insertions(+), 110 deletions(-) diff --git a/test/__snapshots__/index.js.snap b/test/__snapshots__/index.js.snap index 33c4d61b4c..9416cd77db 100644 --- a/test/__snapshots__/index.js.snap +++ b/test/__snapshots__/index.js.snap @@ -423,12 +423,6 @@ Array [ "status": 200, "to": "/.netlify/builders/_ipx", }, - Object { - "force": false, - "from": "/_next/data/build-id/en/404.json", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", - }, Object { "force": false, "from": "/_next/data/build-id/en/500.json", @@ -489,12 +483,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/en/getStaticProps/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/_next/data/build-id/en/getStaticProps/env.json", @@ -531,12 +519,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/en/getStaticProps/withRevalidate/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": true, "from": "/_next/data/build-id/en/getStaticProps/withRevalidate/1.json", @@ -651,12 +633,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/es/404.json", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", - }, Object { "force": false, "from": "/_next/data/build-id/es/500.json", @@ -717,12 +693,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/es/getStaticProps/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/_next/data/build-id/es/getStaticProps/env.json", @@ -759,12 +729,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/es/getStaticProps/withRevalidate/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/_next/data/build-id/es/getStaticProps/withRevalidate/withFallback/:id.json", @@ -843,12 +807,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/fr/404.json", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", - }, Object { "force": false, "from": "/_next/data/build-id/fr/500.json", @@ -909,12 +867,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/fr/getStaticProps/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/_next/data/build-id/fr/getStaticProps/env.json", @@ -951,12 +903,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/_next/data/build-id/fr/getStaticProps/withRevalidate/:id.json", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/_next/data/build-id/fr/getStaticProps/withRevalidate/withFallback/:id.json", @@ -1069,14 +1015,8 @@ Array [ }, Object { "from": "/*", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", - }, - Object { - "force": false, - "from": "/404", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", + "status": 404, + "to": "/server/pages/en/404.html", }, Object { "force": false, @@ -1130,6 +1070,11 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "from": "/en/*", + "status": 404, + "to": "/server/pages/en/404.html", + }, Object { "force": false, "from": "/env", @@ -1143,10 +1088,9 @@ Array [ "to": "/.netlify/functions/___netlify-handler", }, Object { - "force": false, - "from": "/es/404", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", + "from": "/es/*", + "status": 404, + "to": "/server/pages/es/404.html", }, Object { "force": false, @@ -1208,12 +1152,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/es/getStaticProps/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/es/getStaticProps/env", @@ -1250,12 +1188,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/es/getStaticProps/withRevalidate/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/es/getStaticProps/withRevalidate/withFallback/:id", @@ -1341,10 +1273,9 @@ Array [ "to": "/.netlify/functions/___netlify-handler", }, Object { - "force": false, - "from": "/fr/404", - "status": 200, - "to": "/.netlify/functions/___netlify-handler", + "from": "/fr/*", + "status": 404, + "to": "/server/pages/fr/404.html", }, Object { "force": false, @@ -1406,12 +1337,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/fr/getStaticProps/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/fr/getStaticProps/env", @@ -1448,12 +1373,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/fr/getStaticProps/withRevalidate/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/fr/getStaticProps/withRevalidate/withFallback/:id", @@ -1550,12 +1469,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "force": false, - "from": "/getStaticProps/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": false, "from": "/getStaticProps/env", @@ -1592,12 +1505,6 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, - Object { - "force": false, - "from": "/getStaticProps/withRevalidate/:id", - "status": 200, - "to": "/.netlify/builders/___netlify-odb-handler", - }, Object { "force": true, "from": "/getStaticProps/withRevalidate/1", diff --git a/test/index.js b/test/index.js index b2542e2e06..2f5afb4ebb 100644 --- a/test/index.js +++ b/test/index.js @@ -513,8 +513,12 @@ describe('onBuild()', () => { expect(existsSync(handlerFile)).toBeTruthy() expect(existsSync(odbHandlerFile)).toBeTruthy() - expect(readFileSync(handlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'ssr')`) - expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'odb')`) + expect(readFileSync(handlerFile, 'utf8')).toMatch( + `(config, "../../..", pageRoot, prerenderManifest, staticManifest, 'ssr')`, + ) + expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( + `(config, "../../..", pageRoot, prerenderManifest, staticManifest, 'odb')`, + ) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) @@ -1030,12 +1034,12 @@ describe('utility functions', () => { await patchNextFiles(process.cwd()) const serverFile = path.resolve(process.cwd(), 'node_modules', 'next', 'dist', 'server', 'base-server.js') const patchedData = await readFileSync(serverFile, 'utf8') - expect(patchedData.includes('_BYPASS_SSG')).toBeTruthy() + expect(patchedData.includes('private: isPreviewMode && cachedData')).toBeTruthy() await unpatchNextFiles(process.cwd()) const unPatchedData = await readFileSync(serverFile, 'utf8') - expect(unPatchedData.includes('_BYPASS_SSG')).toBeFalsy() + expect(unPatchedData.includes('private: isPreviewMode && cachedData')).toBeFalsy() }) }) From 81d2d8099bb680d09c001371a7f7fe7c03c5632e Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 14:56:54 +0100 Subject: [PATCH 15/22] test: simplify cypress test names --- .../default/dynamic-routes.spec.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index 663139a7fa..f15c8d8147 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -1,19 +1,19 @@ describe('Static Routing', () => { - it('loads show #42 via SSR on a static route', () => { + it('renders correct page via SSR on a static route', () => { cy.request('/getServerSideProps/static/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'ssr') expect(res.body).to.contain('Sleepy Hollow') }) }) - it('loads show #71 from a static file on a static route', () => { + it('serves correct static file on a static route', () => { cy.request('/getStaticProps/static/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Dancing with the Stars') }) }) - it('loads show #71 via ODB on a static route', () => { + it('renders correct page via ODB on a static route', () => { cy.request('/getStaticProps/with-revalidate/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') @@ -23,42 +23,42 @@ describe('Static Routing', () => { }) describe('Dynamic Routing', () => { - it('loads show #1 via SSR on a dynamic route', () => { + it('renders correct page via SSR on a dynamic route', () => { cy.request('/getServerSideProps/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'ssr') expect(res.body).to.contain('Under the Dome') }) }) - it('loads show #1 via SSR on a dynamic catch-all route', () => { + it('renders correct page via SSR on a dynamic catch-all route', () => { cy.request('/getServerSideProps/all/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'ssr') expect(res.body).to.contain('Under the Dome') }) }) - it('loads show #1 from a static file on a prerendered dynamic route with fallback: false', () => { + it('serves correct static file on a prerendered dynamic route with fallback: false', () => { cy.request('/getStaticProps/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Under the Dome') }) }) - it('returns a 404 on a non-prerendered dynamic route with fallback: false', () => { + it('serves custom 404 on a non-prerendered dynamic route with fallback: false', () => { cy.request({ url: '/getStaticProps/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Custom 404') }) }) - it('loads show #1 from a static file on a prerendered dynamic route with fallback: true', () => { + it('serves correct static file on a prerendered dynamic route with fallback: true', () => { cy.request('/getStaticProps/withFallback/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Under the Dome') }) }) - it('loads a fallback page on a non-prerendered dynamic route with fallback: true', () => { + it('renders fallback page via ODB on a non-prerendered dynamic route with fallback: true', () => { cy.request('/getStaticProps/withFallback/3/').then((res) => { expect(res.status).to.eq(200) // expect 'odb' until https://github.com/netlify/pillar-runtime/issues/438 is fixed @@ -67,42 +67,42 @@ describe('Dynamic Routing', () => { expect(res.body).to.contain('Bitten') }) }) - it('loads show #1 from a static file on a prerendered dynamic route with fallback: blocking', () => { + it('serves correct static file on a prerendered dynamic route with fallback: blocking', () => { cy.request('/getStaticProps/withFallbackBlocking/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Under the Dome') }) }) - it('loads show #1 via ODB on a non-prerendered dynamic route with fallback: blocking', () => { + it('renders correct page via ODB on a non-prerendered dynamic route with fallback: blocking', () => { cy.request('/getStaticProps/withFallbackBlocking/3/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'odb') expect(res.body).to.contain('Bitten') }) }) - it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: false', () => { + it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: false', () => { cy.request('/getStaticProps/withRevalidate/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') expect(res.body).to.contain('Under the Dome') }) }) - it('returns a 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { + it('serves custom 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { cy.request({ url: '/getStaticProps/withRevalidate/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) expect(res.headers).to.not.have.property('x-render-mode') expect(res.body).to.contain('Custom 404') }) }) - it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: true', () => { + it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: true', () => { cy.request('/getStaticProps/withRevalidate/withFallback/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') expect(res.body).to.contain('Under the Dome') }) }) - it('loads a fallback page via ODB on a non-prerendered dynamic route with revalidate and fallback: true', () => { + it('renders fallback page via ODB on a non-prerendered dynamic route with revalidate and fallback: true', () => { cy.request('/getStaticProps/withRevalidate/withFallback/3/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') @@ -110,14 +110,14 @@ describe('Dynamic Routing', () => { expect(res.body).to.contain('Bitten') }) }) - it('loads show #1 via ODB on a prerendered dynamic route with revalidate and fallback: blocking', () => { + it('renders correct page via ODB on a prerendered dynamic route with revalidate and fallback: blocking', () => { cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/1/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') expect(res.body).to.contain('Under the Dome') }) }) - it('loads show #1 via ODB on a non-prerendered dynamic route with revalidate and fallback: blocking', () => { + it('renders correct page via ODB on a non-prerendered dynamic route with revalidate and fallback: blocking', () => { cy.request('/getStaticProps/withRevalidate/withFallbackBlocking/3/').then((res) => { expect(res.status).to.eq(200) expect(res.headers).to.have.property('x-render-mode', 'isr') From 94e732c9440acb148700790254324380d385d80e Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 19:59:42 +0100 Subject: [PATCH 16/22] fix: revert header revalidation owing to side-effects --- packages/runtime/src/templates/getHandler.ts | 21 ++++---------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index 1ad3434871..b63e628a86 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -1,7 +1,6 @@ import { HandlerContext, HandlerEvent } from '@netlify/functions' // Aliasing like this means the editor may be able to syntax-highlight the string import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge' -import type { PrerenderManifest } from 'next/dist/build' import { outdent as javascript } from 'outdent' import type { NextConfig } from '../helpers/config' @@ -26,15 +25,8 @@ type Mutable = { } // We return a function and then call `toString()` on it to serialise it as the launcher function -const makeHandler = ( - conf: NextConfig, - app, - pageRoot, - prerenderManifest: PrerenderManifest, - staticManifest: Array<[string, string]> = [], - mode = 'ssr', // eslint-disable-next-line max-params -) => { +const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => { // Change working directory into the site root, unless using Nx, which moves the // dist directory and handles this itself const dir = path.resolve(__dirname, app) @@ -89,10 +81,6 @@ const makeHandler = ( }) const requestHandler = nextServer.getRequestHandler() const server = new Server(async (req, res) => { - if (prerenderManifest.preview) { - // force manual revalidation for fresh content - req.headers['x-prerender-revalidate'] = prerenderManifest.preview.previewModeId - } try { await requestHandler(req, res) } catch (error) { @@ -169,16 +157,15 @@ export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDi const { builder } = require("@netlify/functions"); const { config } = require("${publishDir}/required-server-files.json") - let prerenderManifest, staticManifest + let staticManifest try { - prerenderManifest = require("${publishDir}/prerender-manifest.json") staticManifest = require("${publishDir}/static-manifest.json") } catch {} const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", config.target === "server" ? "server" : "serverless", "pages")); exports.handler = ${ isODB - ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, prerenderManifest, staticManifest, 'odb'));` - : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, prerenderManifest, staticManifest, 'ssr');` + ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));` + : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');` } ` From 7a662ee369e5718cb23b7d577746768ad69dd9b2 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 20:00:53 +0100 Subject: [PATCH 17/22] feat: patch next server to force revalidation only during cache fetches --- packages/runtime/src/helpers/files.ts | 7 ++++++- packages/runtime/src/templates/getHandler.ts | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index fe44ea62b7..603aeefb60 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -333,8 +333,13 @@ const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } -// ensure ISR 404 pages send the correct SWR cache headers const baseServerReplacements: Array<[string, string]> = [ + // force manual revalidate during cache fetches + [ + `checkIsManualRevalidate(req, this.renderOpts.previewProps)`, + `checkIsManualRevalidate(process.env._REVALIDATE_SSG ? { headers: { 'x-prerender-revalidate': this.renderOpts.previewProps.previewModeId } } : req, this.renderOpts.previewProps)`, + ], + // ensure ISR 404 pages send the correct SWR cache headers [`private: isPreviewMode || is404Page && cachedData`, `private: isPreviewMode && cachedData`], ] diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index b63e628a86..6a08e1c226 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -25,7 +25,7 @@ type Mutable = { } // We return a function and then call `toString()` on it to serialise it as the launcher function - // eslint-disable-next-line max-params +// eslint-disable-next-line max-params const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => { // Change working directory into the site root, unless using Nx, which moves the // dist directory and handles this itself @@ -49,7 +49,7 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str conf.experimental.isrFlushToDisk = false // This is our flag that we use when patching the source // eslint-disable-next-line no-underscore-dangle - process.env._BYPASS_SSG = 'true' + process.env._REVALIDATE_SSG = 'true' for (const [key, value] of Object.entries(conf.env)) { process.env[key] = String(value) } From dc424b303f9c12e42e656b761aa1e666c40798c0 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 20:01:17 +0100 Subject: [PATCH 18/22] test: update tests with new env var name --- test/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/index.js b/test/index.js index 2f5afb4ebb..d7783a9a0d 100644 --- a/test/index.js +++ b/test/index.js @@ -1034,11 +1034,13 @@ describe('utility functions', () => { await patchNextFiles(process.cwd()) const serverFile = path.resolve(process.cwd(), 'node_modules', 'next', 'dist', 'server', 'base-server.js') const patchedData = await readFileSync(serverFile, 'utf8') + expect(patchedData.includes('_REVALIDATE_SSG')).toBeTruthy() expect(patchedData.includes('private: isPreviewMode && cachedData')).toBeTruthy() await unpatchNextFiles(process.cwd()) const unPatchedData = await readFileSync(serverFile, 'utf8') + expect(unPatchedData.includes('_REVALIDATE_SSG')).toBeFalsy() expect(unPatchedData.includes('private: isPreviewMode && cachedData')).toBeFalsy() }) }) From eec91b0ccab25d8e287be0784d40b5285482f920 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Tue, 6 Sep 2022 20:07:13 +0100 Subject: [PATCH 19/22] test: update test with reverted handler signature --- test/index.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/index.js b/test/index.js index d7783a9a0d..1a92b6d487 100644 --- a/test/index.js +++ b/test/index.js @@ -513,12 +513,8 @@ describe('onBuild()', () => { expect(existsSync(handlerFile)).toBeTruthy() expect(existsSync(odbHandlerFile)).toBeTruthy() - expect(readFileSync(handlerFile, 'utf8')).toMatch( - `(config, "../../..", pageRoot, prerenderManifest, staticManifest, 'ssr')`, - ) - expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( - `(config, "../../..", pageRoot, prerenderManifest, staticManifest, 'odb')`, - ) + expect(readFileSync(handlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'ssr')`) + expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'odb')`) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) From 8732a49a74de1b716e5443508e299c48fbe9583e Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 7 Sep 2022 16:54:10 +0100 Subject: [PATCH 20/22] fix: reinstate catch-all redirect for Next redirect handling --- .../default/dynamic-routes.spec.ts | 8 +-- packages/runtime/src/helpers/redirects.ts | 55 +++---------------- 2 files changed, 11 insertions(+), 52 deletions(-) diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index f15c8d8147..35f8f379ac 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -44,10 +44,10 @@ describe('Dynamic Routing', () => { expect(res.body).to.contain('Under the Dome') }) }) - it('serves custom 404 on a non-prerendered dynamic route with fallback: false', () => { + it('renders custom 404 on a non-prerendered dynamic route with fallback: false', () => { cy.request({ url: '/getStaticProps/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) - expect(res.headers).to.not.have.property('x-render-mode') + expect(res.headers).to.have.property('x-render-mode', 'ssr') expect(res.body).to.contain('Custom 404') }) }) @@ -88,10 +88,10 @@ describe('Dynamic Routing', () => { expect(res.body).to.contain('Under the Dome') }) }) - it('serves custom 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { + it('renders custom 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { cy.request({ url: '/getStaticProps/withRevalidate/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) - expect(res.headers).to.not.have.property('x-render-mode') + expect(res.headers).to.have.property('x-render-mode', 'ssr') expect(res.body).to.contain('Custom 404') }) }) diff --git a/packages/runtime/src/helpers/redirects.ts b/packages/runtime/src/helpers/redirects.ts index f860abc806..2e950e807e 100644 --- a/packages/runtime/src/helpers/redirects.ts +++ b/packages/runtime/src/helpers/redirects.ts @@ -65,51 +65,6 @@ const generateLocaleRedirects = ({ return redirects } -const generate404Redirects = ({ - staticRouteEntries, - basePath, - i18n, -}: { - staticRouteEntries: Array<[string, SsgRoute]> - basePath: string - i18n: NextConfig['i18n'] -}): NetlifyConfig['redirects'] => { - const redirects: NetlifyConfig['redirects'] = [] - - const isIsr404 = staticRouteEntries.some( - ([route, { initialRevalidateSeconds }]) => is404Route(route, i18n) && initialRevalidateSeconds !== false, - ) - - if (isIsr404) { - redirects.push({ - from: `${basePath}/*`, - to: ODB_FUNCTION_PATH, - status: 404, - }) - } else if (i18n?.locales?.length) { - i18n.locales.forEach((locale) => { - redirects.push({ - from: `${basePath}/${locale}/*`, - to: `${basePath}/server/pages/${locale}/404.html`, - status: 404, - }) - }) - redirects.push({ - from: `${basePath}/*`, - to: `${basePath}/server/pages/${i18n.defaultLocale}/404.html`, - status: 404, - }) - } else { - redirects.push({ - from: `${basePath}/*`, - to: `${basePath}/server/pages/404.html`, - status: 404, - }) - } - - return redirects -} - export const generateStaticRedirects = ({ netlifyConfig, nextConfig: { i18n, basePath }, @@ -249,10 +204,9 @@ const generateDynamicRewrites = ({ return } if (route.page in prerenderedDynamicRoutes) { - const { fallback } = prerenderedDynamicRoutes[route.page] if (matchesMiddleware(middleware, route.page)) { dynamicRoutesThatMatchMiddleware.push(route.page) - } else if (fallback !== false) { + } else { dynamicRewrites.push( ...redirectsForNextRoute({ buildId, route: route.page, basePath, to: ODB_FUNCTION_PATH, status: 200, i18n }), ) @@ -342,7 +296,12 @@ export const generateRedirects = async ({ netlifyConfig.redirects.push(...dynamicRewrites) routesThatMatchMiddleware.push(...dynamicRoutesThatMatchMiddleware) - netlifyConfig.redirects.push(...generate404Redirects({ staticRouteEntries, basePath, i18n })) + // Final fallback + netlifyConfig.redirects.push({ + from: `${basePath}/*`, + to: HANDLER_FUNCTION_PATH, + status: 200, + }) const middlewareMatches = new Set(routesThatMatchMiddleware).size if (middlewareMatches > 0) { From e056209bea137701d27f5b931fffdd4cce023286 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 7 Sep 2022 17:05:07 +0100 Subject: [PATCH 21/22] test: update snapshot with new redirects --- test/__snapshots__/index.js.snap | 91 ++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/test/__snapshots__/index.js.snap b/test/__snapshots__/index.js.snap index 9416cd77db..bb77f6a9a4 100644 --- a/test/__snapshots__/index.js.snap +++ b/test/__snapshots__/index.js.snap @@ -483,6 +483,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/en/getStaticProps/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/_next/data/build-id/en/getStaticProps/env.json", @@ -519,6 +525,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/en/getStaticProps/withRevalidate/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": true, "from": "/_next/data/build-id/en/getStaticProps/withRevalidate/1.json", @@ -693,6 +705,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/es/getStaticProps/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/_next/data/build-id/es/getStaticProps/env.json", @@ -729,6 +747,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/es/getStaticProps/withRevalidate/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/_next/data/build-id/es/getStaticProps/withRevalidate/withFallback/:id.json", @@ -867,6 +891,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/fr/getStaticProps/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/_next/data/build-id/fr/getStaticProps/env.json", @@ -903,6 +933,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/_next/data/build-id/fr/getStaticProps/withRevalidate/:id.json", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/_next/data/build-id/fr/getStaticProps/withRevalidate/withFallback/:id.json", @@ -1015,8 +1051,8 @@ Array [ }, Object { "from": "/*", - "status": 404, - "to": "/server/pages/en/404.html", + "status": 200, + "to": "/.netlify/functions/___netlify-handler", }, Object { "force": false, @@ -1070,11 +1106,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "from": "/en/*", - "status": 404, - "to": "/server/pages/en/404.html", - }, Object { "force": false, "from": "/env", @@ -1087,11 +1118,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "from": "/es/*", - "status": 404, - "to": "/server/pages/es/404.html", - }, Object { "force": false, "from": "/es/500", @@ -1152,6 +1178,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/es/getStaticProps/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/es/getStaticProps/env", @@ -1188,6 +1220,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/es/getStaticProps/withRevalidate/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/es/getStaticProps/withRevalidate/withFallback/:id", @@ -1272,11 +1310,6 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, - Object { - "from": "/fr/*", - "status": 404, - "to": "/server/pages/fr/404.html", - }, Object { "force": false, "from": "/fr/500", @@ -1337,6 +1370,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/fr/getStaticProps/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/fr/getStaticProps/env", @@ -1373,6 +1412,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/fr/getStaticProps/withRevalidate/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/fr/getStaticProps/withRevalidate/withFallback/:id", @@ -1469,6 +1514,12 @@ Array [ "status": 200, "to": "/.netlify/functions/___netlify-handler", }, + Object { + "force": false, + "from": "/getStaticProps/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": false, "from": "/getStaticProps/env", @@ -1505,6 +1556,12 @@ Array [ "status": 200, "to": "/.netlify/builders/___netlify-odb-handler", }, + Object { + "force": false, + "from": "/getStaticProps/withRevalidate/:id", + "status": 200, + "to": "/.netlify/builders/___netlify-odb-handler", + }, Object { "force": true, "from": "/getStaticProps/withRevalidate/1", From 987defd2e1194630f9c5c16b0c344467e5514237 Mon Sep 17 00:00:00 2001 From: Rob Stanford Date: Wed, 7 Sep 2022 17:06:15 +0100 Subject: [PATCH 22/22] test: update cypress tests for correct render mode --- cypress/integration/default/dynamic-routes.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cypress/integration/default/dynamic-routes.spec.ts b/cypress/integration/default/dynamic-routes.spec.ts index 35f8f379ac..f01dca8948 100644 --- a/cypress/integration/default/dynamic-routes.spec.ts +++ b/cypress/integration/default/dynamic-routes.spec.ts @@ -47,7 +47,7 @@ describe('Dynamic Routing', () => { it('renders custom 404 on a non-prerendered dynamic route with fallback: false', () => { cy.request({ url: '/getStaticProps/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) - expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.headers).to.have.property('x-render-mode', 'odb') expect(res.body).to.contain('Custom 404') }) }) @@ -91,7 +91,7 @@ describe('Dynamic Routing', () => { it('renders custom 404 on a non-prerendered dynamic route with revalidate and fallback: false', () => { cy.request({ url: '/getStaticProps/withRevalidate/3/', failOnStatusCode: false }).then((res) => { expect(res.status).to.eq(404) - expect(res.headers).to.have.property('x-render-mode', 'ssr') + expect(res.headers).to.have.property('x-render-mode', 'odb') expect(res.body).to.contain('Custom 404') }) })