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 dbcaf0cb91..c3396bcbf5 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
-
@@ -105,6 +105,79 @@ const Index = ({ shows }) => {
+ Page types
+
)
}
diff --git a/src/helpers/files.js b/src/helpers/files.js
index 907a7d646e..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, cpSync, copy, writeJson, readFile, writeFile } = 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,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 = 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
let middleware
const manifestPath = join(outputDir, 'middleware-manifest.json')
@@ -88,10 +100,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 +227,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 */
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
}