From 62c24a703c37b64aed869ea0ec8c59b560a5facf Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Dec 2021 10:17:59 +0000 Subject: [PATCH 1/3] chore: update demo page links --- demos/default/pages/index.js | 70 +++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/demos/default/pages/index.js b/demos/default/pages/index.js index dbcaf0cb91..83505b1cc4 100644 --- a/demos/default/pages/index.js +++ b/demos/default/pages/index.js @@ -51,7 +51,7 @@ const Index = ({ shows }) => { ))} -

Catch-All Routess

+

Catch-All Routes

+

Page types

+ ) } From e1d6c4eb2cb070c6890fe03a31d0e4ed8e691716 Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Dec 2021 13:42:45 +0000 Subject: [PATCH 2/3] fix: correct handling of CDN files --- demos/default/next.config.js | 4 ++++ demos/default/pages/index.js | 5 +++++ src/helpers/files.js | 17 ++++++++++++---- src/templates/getHandler.js | 39 ++++++++++++++++++++++++++++++------ 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/demos/default/next.config.js b/demos/default/next.config.js index 59274ee24d..c843868c79 100644 --- a/demos/default/next.config.js +++ b/demos/default/next.config.js @@ -31,6 +31,10 @@ module.exports = { destination: '/:path*', }, ], + afterFiles: [{ + source: '/rewriteToStatic', + destination: '/getStaticProps/1', + }] } }, // Redirects allow you to redirect an incoming request path to a different destination path. diff --git a/demos/default/pages/index.js b/demos/default/pages/index.js index 83505b1cc4..c3396bcbf5 100644 --- a/demos/default/pages/index.js +++ b/demos/default/pages/index.js @@ -167,6 +167,11 @@ const Index = ({ shows }) => { Rewrite (should display image) +
  • + + Rewrite to static (should show getStaticProps/1) + +
  • Middleware diff --git a/src/helpers/files.js b/src/helpers/files.js index 907a7d646e..0a76f15248 100644 --- a/src/helpers/files.js +++ b/src/helpers/files.js @@ -2,7 +2,7 @@ const { cpus } = require('os') const { yellowBright } = require('chalk') -const { existsSync, readJson, move, cpSync, copy, writeJson, readFile, writeFile } = require('fs-extra') +const { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir } = require('fs-extra') const globby = require('globby') const { outdent } = require('outdent') const pLimit = require('p-limit') @@ -62,7 +62,9 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => { console.log('Moving static page files to serve from CDN...') const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless') const root = join(outputDir, 'pages') - + const buildId = await readFile(join(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8') + const dataDir = join('_next', 'data', buildId) + await ensureDir(dataDir) // Load the middleware manifest so we can check if a file matches it before moving let middleware const manifestPath = join(outputDir, 'middleware-manifest.json') @@ -88,10 +90,17 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => { }) const files = [] + const filesManifest = {} const moveFile = async (file) => { + const isData = file.endsWith('.json') const source = join(root, file) + const target = isData ? join(dataDir, file) : file + files.push(file) - const dest = join(netlifyConfig.build.publish, file) + filesManifest[file] = target + + const dest = join(netlifyConfig.build.publish, target) + try { await move(source, dest) } catch (error) { @@ -208,7 +217,7 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => { } // Write the manifest for use in the serverless functions - await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), files) + await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), Object.entries(filesManifest)) if (i18n?.defaultLocale) { // Copy the default locale into the root diff --git a/src/templates/getHandler.js b/src/templates/getHandler.js index 1855508fbd..9e79d1f0f5 100644 --- a/src/templates/getHandler.js +++ b/src/templates/getHandler.js @@ -1,3 +1,4 @@ +/* eslint-disable max-lines-per-function */ const { promises, existsSync } = require('fs') const { Server } = require('http') const { tmpdir } = require('os') @@ -34,12 +35,14 @@ const makeHandler = // In most cases these are served from the CDN, but for rewrites Next may try to read them // from disk. We need to intercept these and load them from the CDN instead // Sadly the only way to do this is to monkey-patch fs.promises. Yeah, I know. - const staticFiles = new Set(staticManifest) - + const staticFiles = new Map(staticManifest) + const downloadPromises = new Map() + const statsCache = new Map() // Yes, you can cache stuff locally in a Lambda const cacheDir = path.join(tmpdir(), 'next-static-cache') // Grab the real fs.promises.readFile... const readfileOrig = promises.readFile + const statsOrig = promises.stat // ...then money-patch it to see if it's requesting a CDN file promises.readFile = async (file, options) => { // We only care about page files @@ -51,13 +54,24 @@ const makeHandler = if (staticFiles.has(filePath) && !existsSync(file)) { // This name is safe to use, because it's one that was already created by Next const cacheFile = path.join(cacheDir, filePath) - // Have we already cached it? We ignore the cache if running locally to avoid staleness + const url = `${base}/${staticFiles.get(filePath)}` + + // If it's already downloading we can wait for it to finish + if (downloadPromises.has(url)) { + await downloadPromises.get(url) + } + // Have we already cached it? We download every time if running locally to avoid staleness if ((!existsSync(cacheFile) || process.env.NETLIFY_DEV) && base) { await promises.mkdir(path.dirname(cacheFile), { recursive: true }) - // Append the path to our host and we can load it like a regular page - const url = `${base}/${filePath}` - await downloadFile(url, cacheFile) + try { + // Append the path to our host and we can load it like a regular page + const downloadPromise = downloadFile(url, cacheFile) + downloadPromises.set(url, downloadPromise) + await downloadPromise + } finally { + downloadPromises.delete(url) + } } // Return the cache file return readfileOrig(cacheFile, options) @@ -66,6 +80,18 @@ const makeHandler = return readfileOrig(file, options) } + + promises.stat = async (file, options) => { + // We only care about page files + if (file.startsWith(pageRoot)) { + // We only want the part after `pages/` + const cacheFile = path.join(cacheDir, file.slice(pageRoot.length + 1)) + if (existsSync(cacheFile)) { + return statsOrig(cacheFile, options) + } + } + return statsOrig(file, options) + } } let NextServer try { @@ -183,3 +209,4 @@ exports.handler = ${ ` module.exports = getHandler +/* eslint-enable max-lines-per-function */ From 663f8c7f4757ac33276418ed733f3daca808bb4d Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 2 Dec 2021 14:49:46 +0000 Subject: [PATCH 3/3] chore: fix tests --- src/helpers/files.js | 14 ++- test/__snapshots__/index.js.snap | 200 ++++++++++++++++++++++++------- test/index.js | 12 +- 3 files changed, 179 insertions(+), 47 deletions(-) diff --git a/src/helpers/files.js b/src/helpers/files.js index 0a76f15248..db9135d94d 100644 --- a/src/helpers/files.js +++ b/src/helpers/files.js @@ -2,7 +2,17 @@ const { cpus } = require('os') const { yellowBright } = require('chalk') -const { existsSync, readJson, move, copy, writeJson, readFile, writeFile, ensureDir } = require('fs-extra') +const { + existsSync, + readJson, + move, + copy, + writeJson, + readFile, + writeFile, + ensureDir, + readFileSync, +} = require('fs-extra') const globby = require('globby') const { outdent } = require('outdent') const pLimit = require('p-limit') @@ -62,7 +72,7 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => { console.log('Moving static page files to serve from CDN...') const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless') const root = join(outputDir, 'pages') - const buildId = await readFile(join(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8') + const buildId = readFileSync(join(netlifyConfig.build.publish, 'BUILD_ID'), 'utf8').trim() const dataDir = join('_next', 'data', buildId) await ensureDir(dataDir) // Load the middleware manifest so we can check if a file matches it before moving diff --git a/test/__snapshots__/index.js.snap b/test/__snapshots__/index.js.snap index 75aed6b492..a3ea98ccb1 100644 --- a/test/__snapshots__/index.js.snap +++ b/test/__snapshots__/index.js.snap @@ -72,46 +72,166 @@ exports.resolvePages = () => { exports[`onBuild() generates static files manifest 1`] = ` Array [ - "en/getStaticProps/1.html", - "en/getStaticProps/1.json", - "en/getStaticProps/2.html", - "en/getStaticProps/2.json", - "en/getStaticProps/env.html", - "en/getStaticProps/env.json", - "en/getStaticProps/static.html", - "en/getStaticProps/static.json", - "en/getStaticProps/withFallback/3.html", - "en/getStaticProps/withFallback/3.json", - "en/getStaticProps/withFallback/4.html", - "en/getStaticProps/withFallback/4.json", - "en/getStaticProps/withFallback/my/path/1.html", - "en/getStaticProps/withFallback/my/path/1.json", - "en/getStaticProps/withFallback/my/path/2.html", - "en/getStaticProps/withFallback/my/path/2.json", - "en/getStaticProps/withFallbackBlocking/3.html", - "en/getStaticProps/withFallbackBlocking/3.json", - "en/getStaticProps/withFallbackBlocking/4.html", - "en/getStaticProps/withFallbackBlocking/4.json", - "en/image.html", - "en/previewTest.html", - "en/previewTest.json", - "en/static.html", - "es/getStaticProps/env.html", - "es/getStaticProps/env.json", - "es/getStaticProps/static.html", - "es/getStaticProps/static.json", - "es/image.html", - "es/previewTest.html", - "es/previewTest.json", - "es/static.html", - "fr/getStaticProps/env.html", - "fr/getStaticProps/env.json", - "fr/getStaticProps/static.html", - "fr/getStaticProps/static.json", - "fr/image.html", - "fr/previewTest.html", - "fr/previewTest.json", - "fr/static.html", + Array [ + "en/getStaticProps/1.html", + "en/getStaticProps/1.html", + ], + Array [ + "en/getStaticProps/1.json", + "_next/data/build-id/en/getStaticProps/1.json", + ], + Array [ + "en/getStaticProps/2.html", + "en/getStaticProps/2.html", + ], + Array [ + "en/getStaticProps/2.json", + "_next/data/build-id/en/getStaticProps/2.json", + ], + Array [ + "en/getStaticProps/env.html", + "en/getStaticProps/env.html", + ], + Array [ + "en/getStaticProps/env.json", + "_next/data/build-id/en/getStaticProps/env.json", + ], + Array [ + "en/getStaticProps/static.html", + "en/getStaticProps/static.html", + ], + Array [ + "en/getStaticProps/static.json", + "_next/data/build-id/en/getStaticProps/static.json", + ], + Array [ + "en/getStaticProps/withFallback/3.html", + "en/getStaticProps/withFallback/3.html", + ], + Array [ + "en/getStaticProps/withFallback/3.json", + "_next/data/build-id/en/getStaticProps/withFallback/3.json", + ], + Array [ + "en/getStaticProps/withFallback/4.html", + "en/getStaticProps/withFallback/4.html", + ], + Array [ + "en/getStaticProps/withFallback/4.json", + "_next/data/build-id/en/getStaticProps/withFallback/4.json", + ], + Array [ + "en/getStaticProps/withFallback/my/path/1.html", + "en/getStaticProps/withFallback/my/path/1.html", + ], + Array [ + "en/getStaticProps/withFallback/my/path/1.json", + "_next/data/build-id/en/getStaticProps/withFallback/my/path/1.json", + ], + Array [ + "en/getStaticProps/withFallback/my/path/2.html", + "en/getStaticProps/withFallback/my/path/2.html", + ], + Array [ + "en/getStaticProps/withFallback/my/path/2.json", + "_next/data/build-id/en/getStaticProps/withFallback/my/path/2.json", + ], + Array [ + "en/getStaticProps/withFallbackBlocking/3.html", + "en/getStaticProps/withFallbackBlocking/3.html", + ], + Array [ + "en/getStaticProps/withFallbackBlocking/3.json", + "_next/data/build-id/en/getStaticProps/withFallbackBlocking/3.json", + ], + Array [ + "en/getStaticProps/withFallbackBlocking/4.html", + "en/getStaticProps/withFallbackBlocking/4.html", + ], + Array [ + "en/getStaticProps/withFallbackBlocking/4.json", + "_next/data/build-id/en/getStaticProps/withFallbackBlocking/4.json", + ], + Array [ + "en/image.html", + "en/image.html", + ], + Array [ + "en/previewTest.html", + "en/previewTest.html", + ], + Array [ + "en/previewTest.json", + "_next/data/build-id/en/previewTest.json", + ], + Array [ + "en/static.html", + "en/static.html", + ], + Array [ + "es/getStaticProps/env.html", + "es/getStaticProps/env.html", + ], + Array [ + "es/getStaticProps/env.json", + "_next/data/build-id/es/getStaticProps/env.json", + ], + Array [ + "es/getStaticProps/static.html", + "es/getStaticProps/static.html", + ], + Array [ + "es/getStaticProps/static.json", + "_next/data/build-id/es/getStaticProps/static.json", + ], + Array [ + "es/image.html", + "es/image.html", + ], + Array [ + "es/previewTest.html", + "es/previewTest.html", + ], + Array [ + "es/previewTest.json", + "_next/data/build-id/es/previewTest.json", + ], + Array [ + "es/static.html", + "es/static.html", + ], + Array [ + "fr/getStaticProps/env.html", + "fr/getStaticProps/env.html", + ], + Array [ + "fr/getStaticProps/env.json", + "_next/data/build-id/fr/getStaticProps/env.json", + ], + Array [ + "fr/getStaticProps/static.html", + "fr/getStaticProps/static.html", + ], + Array [ + "fr/getStaticProps/static.json", + "_next/data/build-id/fr/getStaticProps/static.json", + ], + Array [ + "fr/image.html", + "fr/image.html", + ], + Array [ + "fr/previewTest.html", + "fr/previewTest.html", + ], + Array [ + "fr/previewTest.json", + "_next/data/build-id/fr/previewTest.json", + ], + Array [ + "fr/static.html", + "fr/static.html", + ], ] `; diff --git a/test/index.js b/test/index.js index 8534e56e97..702794d463 100644 --- a/test/index.js +++ b/test/index.js @@ -206,8 +206,11 @@ describe('onBuild()', () => { test("fails if BUILD_ID doesn't exist", async () => { await moveNextDist() await unlink(path.join(process.cwd(), '.next/BUILD_ID')) - const failBuild = jest.fn() - await plugin.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } }) + const failBuild = jest.fn().mockImplementation(() => { + throw new Error('BUILD_ID does not exist') + }) + + expect(() => plugin.onBuild({ ...defaultArgs, utils: { ...utils, build: { failBuild } } })).rejects.toThrow() expect(failBuild).toHaveBeenCalled() }) @@ -260,8 +263,7 @@ describe('onBuild()', () => { await moveNextDist() await plugin.onBuild(defaultArgs) const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8')) - - data.forEach((file) => { + data.forEach(([_, file]) => { expect(existsSync(path.resolve(path.join('.next', file)))).toBeTruthy() expect(existsSync(path.resolve(path.join('.next', 'server', 'pages', file)))).toBeFalsy() }) @@ -274,7 +276,7 @@ describe('onBuild()', () => { const locale = 'en/' - data.forEach((file) => { + data.forEach(([_, file]) => { if (!file.startsWith(locale)) { return }