From ae3c38803999fa76a7f6814ef8fc7e56bce17ea7 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 29 May 2020 11:33:09 -0500 Subject: [PATCH] Add support for rewriting non-fallback SSG pages (#11010) Since non-fallback pages don't rely on the URL for hydration we can allow them to be rewritten to but pages with fallback still can't be rewritten to because we won't be able to parse the correct `/_next/data` path to request the page's data from. I added a test for this behavior and ensured it works correctly on Now. Example on with fallback false rewrite on Now: https://tst-rewrite-cp9vge4bg.now.sh/about --- .../next/next-server/server/next-server.ts | 9 +- test/integration/prerender/next.config.js | 12 +++ .../prerender/pages/lang/[lang]/about.js | 12 +++ test/integration/prerender/test/index.test.js | 87 +++++++++++++++++-- 4 files changed, 110 insertions(+), 10 deletions(-) create mode 100644 test/integration/prerender/next.config.js create mode 100644 test/integration/prerender/pages/lang/[lang]/about.js diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 5f9fbd23fb652..927071d470b2e 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -580,6 +580,7 @@ export default class Server { } } ;(req as any)._nextDidRewrite = true + ;(req as any)._nextRewroteUrl = newUrl return { finished: false, @@ -970,8 +971,12 @@ export default class Server { isPreviewMode = previewData !== false } - // Compute the iSSG cache key - let urlPathname = `${parseUrl(req.url || '').pathname!}` + // Compute the iSSG cache key. We use the rewroteUrl since + // pages with fallback: false are allowed to be rewritten to + // and we need to look up the path by the rewritten path + let urlPathname = (req as any)._nextRewroteUrl + ? (req as any)._nextRewroteUrl + : `${parseUrl(req.url || '').pathname!}` // remove /_next/data prefix from urlPathname so it matches // for direct page visit and /_next/data visit diff --git a/test/integration/prerender/next.config.js b/test/integration/prerender/next.config.js new file mode 100644 index 0000000000000..edfaf6892fe20 --- /dev/null +++ b/test/integration/prerender/next.config.js @@ -0,0 +1,12 @@ +module.exports = { + experimental: { + rewrites() { + return [ + { + source: '/about', + destination: '/lang/en/about', + }, + ] + }, + }, +} diff --git a/test/integration/prerender/pages/lang/[lang]/about.js b/test/integration/prerender/pages/lang/[lang]/about.js new file mode 100644 index 0000000000000..693a814883c73 --- /dev/null +++ b/test/integration/prerender/pages/lang/[lang]/about.js @@ -0,0 +1,12 @@ +export default ({ lang }) =>

About: {lang}

+ +export const getStaticProps = ({ params: { lang } }) => ({ + props: { + lang, + }, +}) + +export const getStaticPaths = () => ({ + paths: ['en', 'es', 'fr', 'de'].map((p) => `/lang/${p}/about`), + fallback: false, +}) diff --git a/test/integration/prerender/test/index.test.js b/test/integration/prerender/test/index.test.js index 819bae4aadeae..04e89917eca15 100644 --- a/test/integration/prerender/test/index.test.js +++ b/test/integration/prerender/test/index.test.js @@ -36,6 +36,7 @@ let buildId let distPagesDir let exportDir let stderr +let origConfig const startServer = async (optEnv = {}) => { const scriptPath = join(appDir, 'server.js') @@ -130,6 +131,26 @@ const expectedManifestRoutes = () => ({ initialRevalidateSeconds: false, srcRoute: null, }, + '/lang/de/about': { + dataRoute: `/_next/data/${buildId}/lang/de/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/en/about': { + dataRoute: `/_next/data/${buildId}/lang/en/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/es/about': { + dataRoute: `/_next/data/${buildId}/lang/es/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, + '/lang/fr/about': { + dataRoute: `/_next/data/${buildId}/lang/fr/about.json`, + initialRevalidateSeconds: false, + srcRoute: '/lang/[lang]/about', + }, '/something': { dataRoute: `/_next/data/${buildId}/something.json`, initialRevalidateSeconds: false, @@ -492,6 +513,11 @@ const runTests = (dev = false, looseMode = false) => { const html = await res.text() expect(html).toMatch(/This page could not be found/) }) + + it('should allow rewriting to SSG page with fallback: false', async () => { + const html = await renderViaHTTP(appPort, '/about') + expect(html).toMatch(/About:.*?en/) + }) } if (dev) { @@ -828,6 +854,18 @@ const runTests = (dev = false, looseMode = false) => { ), page: '/default-revalidate', }, + { + namedDataRouteRegex: `^/_next/data/${escapeRegex( + buildId + )}/lang/(?[^/]+?)/about\\.json$`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/lang\\/([^\\/]+?)\\/about\\.json$` + ), + page: '/lang/[lang]/about', + routeKeys: ['lang'], + }, { namedDataRouteRegex: `^/_next/data/${escapeRegex( buildId @@ -899,6 +937,14 @@ const runTests = (dev = false, looseMode = false) => { '^\\/blog\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$' ), }, + '/lang/[lang]/about': { + dataRoute: `/_next/data/${buildId}/lang/[lang]/about.json`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapedBuildId}\\/lang\\/([^\\/]+?)\\/about\\.json$` + ), + fallback: false, + routeRegex: normalizeRegEx('^\\/lang\\/([^\\/]+?)\\/about(?:\\/)?$'), + }, '/non-json/[p]': { dataRoute: `/_next/data/${buildId}/non-json/[p].json`, dataRouteRegex: normalizeRegEx( @@ -1039,10 +1085,9 @@ const runTests = (dev = false, looseMode = false) => { } describe('SSG Prerender', () => { - afterAll(() => fs.remove(nextConfig)) - describe('dev mode', () => { beforeAll(async () => { + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, ` @@ -1053,6 +1098,10 @@ describe('SSG Prerender', () => { { source: "/some-rewrite/:item", destination: "/blog/post-:item" + }, + { + source: '/about', + destination: '/lang/en/about' } ] } @@ -1069,13 +1118,17 @@ describe('SSG Prerender', () => { }) buildId = 'development' }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) + await killApp(app) + }) runTests(true) }) describe('dev mode getStaticPaths', () => { beforeAll(async () => { + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, // we set cpus to 1 so that we make sure the requests @@ -1090,7 +1143,7 @@ describe('SSG Prerender', () => { }) }) afterAll(async () => { - await fs.remove(nextConfig) + await fs.writeFile(nextConfig, origConfig) await killApp(app) }) @@ -1133,6 +1186,7 @@ describe('SSG Prerender', () => { beforeAll(async () => { // remove firebase import since it breaks in legacy serverless mode origBlogPageContent = await fs.readFile(blogPagePath, 'utf8') + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( blogPagePath, @@ -1144,7 +1198,19 @@ describe('SSG Prerender', () => { await fs.writeFile( nextConfig, - `module.exports = { target: 'serverless' }`, + `module.exports = { + target: 'serverless', + experimental: { + rewrites() { + return [ + { + source: '/about', + destination: '/lang/en/about' + } + ] + } + } + }`, 'utf8' ) await fs.remove(join(appDir, '.next')) @@ -1160,6 +1226,7 @@ describe('SSG Prerender', () => { buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) await fs.writeFile(blogPagePath, origBlogPageContent) await killApp(app) }) @@ -1234,6 +1301,7 @@ describe('SSG Prerender', () => { return initNextServerScript(scriptPath, /ready on/i, env) } + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, `module.exports = { target: 'experimental-serverless-trace' }`, @@ -1249,7 +1317,10 @@ describe('SSG Prerender', () => { appPort = await findPort() app = await startServerlessEmulator(appDir, appPort, buildId) }) - afterAll(() => killApp(app)) + afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) + await killApp(app) + }) runTests(false, true) }) @@ -1257,7 +1328,6 @@ describe('SSG Prerender', () => { describe('production mode', () => { let buildOutput = '' beforeAll(async () => { - await fs.remove(nextConfig) await fs.remove(join(appDir, '.next')) const { stdout } = await nextBuild(appDir, [], { stdout: true }) buildOutput = stdout @@ -1296,6 +1366,7 @@ describe('SSG Prerender', () => { beforeAll(async () => { exportDir = join(appDir, 'out') + origConfig = await fs.readFile(nextConfig, 'utf8') await fs.writeFile( nextConfig, `module.exports = { @@ -1329,8 +1400,8 @@ describe('SSG Prerender', () => { buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { + await fs.writeFile(nextConfig, origConfig) await stopApp(app) - await fs.remove(nextConfig) for (const page of fallbackTruePages) { const pagePath = join(appDir, 'pages', page)