From f0ead094216b04cb3c781e3bbef9359aa680c929 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 16 Oct 2020 04:27:34 -0500 Subject: [PATCH] Fix initialRevalidateSeconds manifest field with i18n (#17926) This makes sure the correct `initialRevalidateSeconds` field is populated in the `prerender-manifest` for non-dynamic SSG pages since they will be inserted into the `initialPageRevalidationMap` under their locale prefixed variant with `i18n` enabled x-ref: https://github.com/vercel/next.js/pull/17370 --- packages/next/build/index.ts | 10 +- .../webpack/loaders/next-serverless-loader.ts | 21 +++- .../i18n-support/test/index.test.js | 104 +++++++++++++++++- 3 files changed, 127 insertions(+), 8 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 15e66148176a04e..05f371f6d910309 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -959,13 +959,19 @@ export default async function build( } if (isSsg) { + const { i18n } = config.experimental + // For a non-dynamic SSG page, we must copy its data file from export. if (!isDynamic) { await moveExportedPage(page, page, file, true, 'json') + const revalidationMapPath = i18n + ? `/${i18n.defaultLocale}${page}` + : page + finalPrerenderRoutes[page] = { initialRevalidateSeconds: - exportConfig.initialPageRevalidationMap[page], + exportConfig.initialPageRevalidationMap[revalidationMapPath], srcRoute: null, dataRoute: path.posix.join('/_next/data', buildId, `${file}.json`), } @@ -973,7 +979,7 @@ export default async function build( const pageInfo = pageInfos.get(page) if (pageInfo) { pageInfo.initialRevalidateSeconds = - exportConfig.initialPageRevalidationMap[page] + exportConfig.initialPageRevalidationMap[revalidationMapPath] pageInfos.set(page, pageInfo) } } else { diff --git a/packages/next/build/webpack/loaders/next-serverless-loader.ts b/packages/next/build/webpack/loaders/next-serverless-loader.ts index 4d56c172e8080f9..53a18a0409ab4af 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader.ts @@ -248,6 +248,8 @@ const nextServerlessLoader: loader.Loader = function () { let localeDomainRedirect const localePathResult = normalizeLocalePath(parsedUrl.pathname, i18n.locales) + routeNoAssetPath = normalizeLocalePath(routeNoAssetPath, i18n.locales).pathname + if (localePathResult.detectedLocale) { detectedLocale = localePathResult.detectedLocale req.url = formatUrl({ @@ -337,6 +339,7 @@ const nextServerlessLoader: loader.Loader = function () { res.end() return } + detectedLocale = detectedLocale || defaultLocale ` : ` @@ -547,10 +550,10 @@ const nextServerlessLoader: loader.Loader = function () { if (parsedUrl.pathname.match(/_next\\/data/)) { const { - default: getrouteNoAssetPath, + default: getRouteNoAssetPath, } = require('next/dist/next-server/lib/router/utils/get-route-from-asset-path'); _nextData = true; - parsedUrl.pathname = getrouteNoAssetPath( + parsedUrl.pathname = getRouteNoAssetPath( parsedUrl.pathname.replace( new RegExp('/_next/data/${escapedBuildId}/'), '/' @@ -609,12 +612,24 @@ const nextServerlessLoader: loader.Loader = function () { ? `const nowParams = req.headers && req.headers["x-now-route-matches"] ? getRouteMatcher( (function() { - const { re, groups } = getRouteRegex("${page}"); + const { re, groups, routeKeys } = getRouteRegex("${page}"); return { re: { // Simulate a RegExp match from the \`req.url\` input exec: str => { const obj = parseQs(str); + + // favor named matches if available + const routeKeyNames = Object.keys(routeKeys) + + if (routeKeyNames.every(name => obj[name])) { + return routeKeyNames.reduce((prev, keyName) => { + const paramName = routeKeys[keyName] + prev[groups[paramName].pos] = obj[keyName] + return prev + }, {}) + } + return Object.keys(obj).reduce( (prev, key) => Object.assign(prev, { diff --git a/test/integration/i18n-support/test/index.test.js b/test/integration/i18n-support/test/index.test.js index 57bba02a8bcd6d5..c375fc27b3d4a2e 100644 --- a/test/integration/i18n-support/test/index.test.js +++ b/test/integration/i18n-support/test/index.test.js @@ -5,6 +5,7 @@ import fs from 'fs-extra' import cheerio from 'cheerio' import { join } from 'path' import webdriver from 'next-webdriver' +import escapeRegex from 'escape-string-regexp' import { fetchViaHTTP, findPort, @@ -15,6 +16,7 @@ import { renderViaHTTP, File, waitFor, + normalizeRegEx, } from 'next-test-utils' jest.setTimeout(1000 * 60 * 2) @@ -24,7 +26,7 @@ const nextConfig = new File(join(appDir, 'next.config.js')) let app let appPort let buildPagesDir -// let buildId +let buildId const locales = ['en-US', 'nl-NL', 'nl-BE', 'nl', 'fr-BE', 'fr', 'en'] @@ -59,6 +61,102 @@ function runTests(isDev) { ], }) }) + + it('should output correct prerender-manifest', async () => { + const prerenderManifest = await fs.readJSON( + join(appDir, '.next/prerender-manifest.json') + ) + + for (const key of Object.keys(prerenderManifest.dynamicRoutes)) { + const item = prerenderManifest.dynamicRoutes[key] + item.routeRegex = normalizeRegEx(item.routeRegex) + item.dataRouteRegex = normalizeRegEx(item.dataRouteRegex) + } + + expect(prerenderManifest.routes).toEqual({ + '/en-US/gsp/fallback/first': { + dataRoute: `/_next/data/${buildId}/en-US/gsp/fallback/first.json`, + initialRevalidateSeconds: false, + srcRoute: '/gsp/fallback/[slug]', + }, + '/en-US/gsp/fallback/second': { + dataRoute: `/_next/data/${buildId}/en-US/gsp/fallback/second.json`, + initialRevalidateSeconds: false, + srcRoute: '/gsp/fallback/[slug]', + }, + '/en-US/gsp/no-fallback/first': { + dataRoute: `/_next/data/${buildId}/en-US/gsp/no-fallback/first.json`, + initialRevalidateSeconds: false, + srcRoute: '/gsp/no-fallback/[slug]', + }, + '/en-US/gsp/no-fallback/second': { + dataRoute: `/_next/data/${buildId}/en-US/gsp/no-fallback/second.json`, + initialRevalidateSeconds: false, + srcRoute: '/gsp/no-fallback/[slug]', + }, + '/en-US/not-found/fallback/first': { + dataRoute: `/_next/data/${buildId}/en-US/not-found/fallback/first.json`, + initialRevalidateSeconds: false, + srcRoute: '/not-found/fallback/[slug]', + }, + '/en-US/not-found/fallback/second': { + dataRoute: `/_next/data/${buildId}/en-US/not-found/fallback/second.json`, + initialRevalidateSeconds: false, + srcRoute: '/not-found/fallback/[slug]', + }, + '/gsp': { + dataRoute: `/_next/data/${buildId}/gsp.json`, + srcRoute: null, + initialRevalidateSeconds: false, + }, + '/nl-NL/gsp/no-fallback/second': { + dataRoute: `/_next/data/${buildId}/nl-NL/gsp/no-fallback/second.json`, + initialRevalidateSeconds: false, + srcRoute: '/gsp/no-fallback/[slug]', + }, + '/not-found': { + dataRoute: `/_next/data/${buildId}/not-found.json`, + srcRoute: null, + initialRevalidateSeconds: false, + }, + }) + expect(prerenderManifest.dynamicRoutes).toEqual({ + '/gsp/fallback/[slug]': { + routeRegex: normalizeRegEx( + '^\\/gsp\\/fallback\\/([^\\/]+?)(?:\\/)?$' + ), + dataRoute: `/_next/data/${buildId}/gsp/fallback/[slug].json`, + fallback: '/gsp/fallback/[slug].html', + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/gsp\\/fallback\\/([^\\/]+?)\\.json$` + ), + }, + '/gsp/no-fallback/[slug]': { + routeRegex: normalizeRegEx( + '^\\/gsp\\/no\\-fallback\\/([^\\/]+?)(?:\\/)?$' + ), + dataRoute: `/_next/data/${buildId}/gsp/no-fallback/[slug].json`, + fallback: false, + dataRouteRegex: normalizeRegEx( + `^/_next/data/${escapeRegex( + buildId + )}/gsp/no\\-fallback/([^/]+?)\\.json$` + ), + }, + '/not-found/fallback/[slug]': { + dataRoute: `/_next/data/${buildId}/not-found/fallback/[slug].json`, + dataRouteRegex: normalizeRegEx( + `^\\/_next\\/data\\/${escapeRegex( + buildId + )}\\/not\\-found\\/fallback\\/([^\\/]+?)\\.json$` + ), + fallback: '/not-found/fallback/[slug].html', + routeRegex: normalizeRegEx('^/not\\-found/fallback/([^/]+?)(?:/)?$'), + }, + }) + }) } it('should navigate with locale prop correctly', async () => { @@ -1145,7 +1243,7 @@ describe('i18n Support', () => { appPort = await findPort() app = await nextStart(appDir, appPort) buildPagesDir = join(appDir, '.next/server/pages') - // buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(() => killApp(app)) @@ -1161,7 +1259,7 @@ describe('i18n Support', () => { appPort = await findPort() app = await nextStart(appDir, appPort) buildPagesDir = join(appDir, '.next/serverless/pages') - // buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') }) afterAll(async () => { nextConfig.restore()