Skip to content

Commit 615c97a

Browse files
feat: don't move files to CDN if they match middleware (#812)
* feat: don't move files to CDN if they match middleware * chore: lint Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 4efce75 commit 615c97a

File tree

3 files changed

+164
-8
lines changed

3 files changed

+164
-8
lines changed

src/helpers/files.js

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// @ts-check
22
const { cpus } = require('os')
33

4+
const { yellowBright } = require('chalk')
45
const { existsSync, readJson, move, cpSync, copy, writeJson } = require('fs-extra')
56
const globby = require('globby')
7+
const { outdent } = require('outdent')
68
const pLimit = require('p-limit')
79
const { join } = require('pathe')
810
const slash = require('slash')
@@ -11,12 +13,41 @@ const TEST_ROUTE = /(|\/)\[[^/]+?](\/|\.html|$)/
1113

1214
const isDynamicRoute = (route) => TEST_ROUTE.test(route)
1315

14-
exports.moveStaticPages = async ({ netlifyConfig, target, i18n, failBuild }) => {
16+
const stripLocale = (rawPath, locales = []) => {
17+
const [locale, ...segments] = rawPath.split('/')
18+
if (locales.includes(locale)) {
19+
return segments.join('/')
20+
}
21+
return rawPath
22+
}
23+
24+
const matchMiddleware = (middleware, filePath) =>
25+
middleware.includes('') ||
26+
middleware?.find(
27+
(middlewarePath) =>
28+
filePath === middlewarePath || filePath === `${middlewarePath}.html` || filePath.startsWith(`${middlewarePath}/`),
29+
)
30+
31+
exports.matchMiddleware = matchMiddleware
32+
exports.stripLocale = stripLocale
33+
exports.isDynamicRoute = isDynamicRoute
34+
35+
exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
1536
console.log('Moving static page files to serve from CDN...')
16-
const root = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless', 'pages')
37+
const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless')
38+
const root = join(outputDir, 'pages')
1739

18-
const files = []
40+
// Load the middleware manifest so we can check if a file matches it before moving
41+
let middleware
42+
const manifestPath = join(outputDir, 'middleware-manifest.json')
43+
if (existsSync(manifestPath)) {
44+
const manifest = await readJson(manifestPath)
45+
if (manifest?.middleware) {
46+
middleware = Object.keys(manifest.middleware).map((path) => path.slice(1))
47+
}
48+
}
1949

50+
const files = []
2051
const moveFile = async (file) => {
2152
const source = join(root, file)
2253
files.push(file)
@@ -29,18 +60,58 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n, failBuild }) =>
2960
dot: true,
3061
})
3162

63+
const matchingMiddleware = new Set()
64+
const matchedPages = new Set()
65+
3266
// Limit concurrent file moves to number of cpus or 2 if there is only 1
3367
const limit = pLimit(Math.max(2, cpus().length))
3468
const promises = pages.map(async (rawPath) => {
3569
const filePath = slash(rawPath)
3670
if (isDynamicRoute(filePath)) {
3771
return
3872
}
73+
// Middleware matches against the unlocalised path
74+
const unlocalizedPath = stripLocale(rawPath, i18n?.locales)
75+
const middlewarePath = matchMiddleware(middleware, unlocalizedPath)
76+
// If a file matches middleware it can't be offloaded to the CDN, and needs to stay at the origin to be served by next/server
77+
if (middlewarePath) {
78+
matchingMiddleware.add(middlewarePath)
79+
matchedPages.add(rawPath)
80+
return
81+
}
3982
return limit(moveFile, filePath)
4083
})
4184
await Promise.all(promises)
4285
console.log(`Moved ${files.length} files`)
4386

87+
if (matchedPages.size !== 0) {
88+
console.log(
89+
yellowBright(outdent`
90+
Skipped moving ${matchedPages.size} ${
91+
matchedPages.size === 1 ? 'file because it matches' : 'files because they match'
92+
} middleware, so cannot be deployed to the CDN and will be served from the origin instead. This is fine, but we're letting you know because it may not be what you expect.
93+
`),
94+
)
95+
96+
console.log(
97+
outdent`
98+
The following middleware matched statically-rendered pages:
99+
100+
${yellowBright([...matchingMiddleware].map((mid) => `- /${mid}/_middleware`).join('\n'))}
101+
`,
102+
)
103+
// There could potentially be thousands of matching pages, so we don't want to spam the console with this
104+
if (matchedPages.size < 50) {
105+
console.log(
106+
outdent`
107+
The following files matched middleware and were not moved to the CDN:
108+
109+
${yellowBright([...matchedPages].map((mid) => `- ${mid}`).join('\n'))}
110+
`,
111+
)
112+
}
113+
}
114+
44115
// Write the manifest for use in the serverless functions
45116
await writeJson(join(netlifyConfig.build.publish, 'static-manifest.json'), files)
46117

test/__snapshots__/index.js.snap

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ Array [
9999
"en/getStaticProps/withRevalidate/withFallback/2.html",
100100
"en/getStaticProps/withRevalidate/withFallback/2.json",
101101
"en/image.html",
102-
"en/middle.html",
103102
"en/previewTest.html",
104103
"en/previewTest.json",
105104
"en/static.html",
@@ -108,7 +107,6 @@ Array [
108107
"es/getStaticProps/with-revalidate.html",
109108
"es/getStaticProps/with-revalidate.json",
110109
"es/image.html",
111-
"es/middle.html",
112110
"es/previewTest.html",
113111
"es/previewTest.json",
114112
"es/static.html",
@@ -117,7 +115,6 @@ Array [
117115
"fr/getStaticProps/with-revalidate.html",
118116
"fr/getStaticProps/with-revalidate.json",
119117
"fr/image.html",
120-
"fr/middle.html",
121118
"fr/previewTest.html",
122119
"fr/previewTest.json",
123120
"fr/static.html",

test/index.js

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const plugin = require('../src')
99

1010
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } = require('../src/constants')
1111
const { join } = require('pathe')
12+
const { matchMiddleware, stripLocale } = require('../src/helpers/files')
1213

1314
const FIXTURES_DIR = `${__dirname}/fixtures`
1415
const SAMPLE_PROJECT_DIR = `${__dirname}/../demo`
@@ -208,8 +209,9 @@ describe('onBuild()', () => {
208209
await moveNextDist()
209210
process.env.EXPERIMENTAL_MOVE_STATIC_PAGES = 'true'
210211
await plugin.onBuild(defaultArgs)
211-
expect(existsSync(path.resolve('.next/static-manifest.json'))).toBeTruthy()
212-
const data = JSON.parse(readFileSync(path.resolve('.next/static-manifest.json'), 'utf8')).sort()
212+
const manifestPath = path.resolve('.next/static-manifest.json')
213+
expect(existsSync(manifestPath)).toBeTruthy()
214+
const data = (await readJson(manifestPath)).sort()
213215
expect(data).toMatchSnapshot()
214216
delete process.env.EXPERIMENTAL_MOVE_STATIC_PAGES
215217
})
@@ -247,6 +249,17 @@ describe('onBuild()', () => {
247249
delete process.env.EXPERIMENTAL_MOVE_STATIC_PAGES
248250
})
249251

252+
test('skips static files that match middleware', async () => {
253+
await moveNextDist()
254+
process.env.EXPERIMENTAL_MOVE_STATIC_PAGES = 'true'
255+
await plugin.onBuild(defaultArgs)
256+
257+
expect(existsSync(path.resolve(path.join('.next', 'en', 'middle.html')))).toBeFalsy()
258+
expect(existsSync(path.resolve(path.join('.next', 'server', 'pages', 'en', 'middle.html')))).toBeTruthy()
259+
260+
delete process.env.EXPERIMENTAL_MOVE_STATIC_PAGES
261+
})
262+
250263
test('sets correct config', async () => {
251264
await moveNextDist()
252265

@@ -355,3 +368,78 @@ describe('onPostBuild', () => {
355368
console.log = oldLog
356369
})
357370
})
371+
372+
describe('utility functions', () => {
373+
test('middleware tester matches correct paths', () => {
374+
const middleware = ['middle', 'sub/directory']
375+
const paths = [
376+
'middle.html',
377+
'middle',
378+
'middle/',
379+
'middle/ware',
380+
'sub/directory',
381+
'sub/directory.html',
382+
'sub/directory/child',
383+
'sub/directory/child.html',
384+
]
385+
for (const path of paths) {
386+
expect(matchMiddleware(middleware, path)).toBeTruthy()
387+
}
388+
})
389+
390+
test('middleware tester does not match incorrect paths', () => {
391+
const middleware = ['middle', 'sub/directory']
392+
const paths = [
393+
'middl',
394+
'',
395+
'somethingelse',
396+
'another.html',
397+
'another/middle.html',
398+
'sub/anotherdirectory.html',
399+
'sub/directoryelse',
400+
'sub/directoryelse.html',
401+
]
402+
for (const path of paths) {
403+
expect(matchMiddleware(middleware, path)).toBeFalsy()
404+
}
405+
})
406+
407+
test('middleware tester matches root middleware', () => {
408+
const middleware = ['']
409+
const paths = [
410+
'middl',
411+
'',
412+
'somethingelse',
413+
'another.html',
414+
'another/middle.html',
415+
'sub/anotherdirectory.html',
416+
'sub/directoryelse',
417+
'sub/directoryelse.html',
418+
]
419+
for (const path of paths) {
420+
expect(matchMiddleware(middleware, path)).toBeTruthy()
421+
}
422+
})
423+
424+
test('stripLocale correctly strips matching locales', () => {
425+
const locales = ['en', 'fr', 'en-GB']
426+
const paths = [
427+
['en/file.html', 'file.html'],
428+
['fr/file.html', 'file.html'],
429+
['en-GB/file.html', 'file.html'],
430+
['file.html', 'file.html'],
431+
]
432+
433+
for (const [path, expected] of paths) {
434+
expect(stripLocale(path, locales)).toEqual(expected)
435+
}
436+
})
437+
438+
test('stripLocale does not touch non-matching matching locales', () => {
439+
const locales = ['en', 'fr', 'en-GB']
440+
const paths = ['de/file.html', 'enfile.html', 'en-US/file.html']
441+
for (const path of paths) {
442+
expect(stripLocale(path, locales)).toEqual(path)
443+
}
444+
})
445+
})

0 commit comments

Comments
 (0)