Skip to content

Commit 9e3dd0e

Browse files
authored
fix: don't move files to the CDN if they match redirect/rewrite rules (#832)
* fix: don't move files to the CDN if they match redirects or beforeFiles rewrites * chore: add tests Release-As: 4.0.0-beta.10
1 parent 6ff3100 commit 9e3dd0e

File tree

5 files changed

+175
-7
lines changed

5 files changed

+175
-7
lines changed

demo/next.config.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module.exports = {
44
generateBuildId: () => 'build-id',
55
i18n: {
66
defaultLocale: 'en',
7-
locales: ['en', 'es', 'fr']
7+
locales: ['en', 'es', 'fr'],
88
},
99
async headers() {
1010
return [
@@ -14,7 +14,7 @@ module.exports = {
1414
{
1515
key: 'x-custom-header',
1616
value: 'my custom header value',
17-
}
17+
},
1818
],
1919
},
2020
]
@@ -29,8 +29,8 @@ module.exports = {
2929
{
3030
source: '/old/:path*',
3131
destination: '/:path*',
32-
}
33-
]
32+
},
33+
],
3434
}
3535
},
3636
// Redirects allow you to redirect an incoming request path to a different destination path.

demo/pages/old/image.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const Images = () => (
2+
<div>
3+
<p>If you can see this, rewrites aren&apos;t working</p>
4+
</div>
5+
)
6+
7+
export default Images

demo/pages/redirectme.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const Redirect = () => (
2+
<div>
3+
<p>If you can see this, redirects aren&apos;t working</p>
4+
</div>
5+
)
6+
7+
export default Redirect

src/helpers/files.js

+77-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// @ts-check
1+
/* eslint-disable max-lines */
22
const { cpus } = require('os')
33

44
const { yellowBright } = require('chalk')
@@ -28,10 +28,36 @@ const matchMiddleware = (middleware, filePath) =>
2828
filePath === middlewarePath || filePath === `${middlewarePath}.html` || filePath.startsWith(`${middlewarePath}/`),
2929
)
3030

31+
const matchesRedirect = (file, redirects) => {
32+
if (!Array.isArray(redirects)) {
33+
return false
34+
}
35+
return redirects.some((redirect) => {
36+
if (!redirect.regex || redirect.internal) {
37+
return false
38+
}
39+
// Strips the extension from the file path
40+
return new RegExp(redirect.regex).test(`/${file.slice(0, -5)}`)
41+
})
42+
}
43+
44+
const matchesRewrite = (file, rewrites) => {
45+
if (Array.isArray(rewrites)) {
46+
return matchesRedirect(file, rewrites)
47+
}
48+
if (!Array.isArray(rewrites?.beforeFiles)) {
49+
return false
50+
}
51+
return matchesRedirect(file, rewrites.beforeFiles)
52+
}
53+
54+
exports.matchesRedirect = matchesRedirect
55+
exports.matchesRewrite = matchesRewrite
3156
exports.matchMiddleware = matchMiddleware
3257
exports.stripLocale = stripLocale
3358
exports.isDynamicRoute = isDynamicRoute
3459

60+
// eslint-disable-next-line max-lines-per-function
3561
exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
3662
console.log('Moving static page files to serve from CDN...')
3763
const outputDir = join(netlifyConfig.build.publish, target === 'server' ? 'server' : 'serverless')
@@ -48,6 +74,7 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
4874
}
4975

5076
const prerenderManifest = await readJson(join(netlifyConfig.build.publish, 'prerender-manifest.json'))
77+
const { redirects, rewrites } = await readJson(join(netlifyConfig.build.publish, 'routes-manifest.json'))
5178

5279
const isrFiles = new Set()
5380

@@ -79,6 +106,8 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
79106

80107
const matchingMiddleware = new Set()
81108
const matchedPages = new Set()
109+
const matchedRedirects = new Set()
110+
const matchedRewrites = new Set()
82111

83112
// Limit concurrent file moves to number of cpus or 2 if there is only 1
84113
const limit = pLimit(Math.max(2, cpus().length))
@@ -91,6 +120,14 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
91120
if (isDynamicRoute(filePath)) {
92121
return
93122
}
123+
if (matchesRedirect(filePath, redirects)) {
124+
matchedRedirects.add(filePath)
125+
return
126+
}
127+
if (matchesRewrite(filePath, rewrites)) {
128+
matchedRewrites.add(filePath)
129+
return
130+
}
94131
// Middleware matches against the unlocalised path
95132
const unlocalizedPath = stripLocale(rawPath, i18n?.locales)
96133
const middlewarePath = matchMiddleware(middleware, unlocalizedPath)
@@ -110,7 +147,8 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
110147
yellowBright(outdent`
111148
Skipped moving ${matchedPages.size} ${
112149
matchedPages.size === 1 ? 'file because it matches' : 'files because they match'
113-
} 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.
150+
} middleware, so cannot be deployed to the CDN and will be served from the origin instead.
151+
This is fine, but we're letting you know because it may not be what you expect.
114152
`),
115153
)
116154

@@ -119,6 +157,8 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
119157
The following middleware matched statically-rendered pages:
120158
121159
${yellowBright([...matchingMiddleware].map((mid) => `- /${mid}/_middleware`).join('\n'))}
160+
161+
────────────────────────────────────────────────────────────────
122162
`,
123163
)
124164
// There could potentially be thousands of matching pages, so we don't want to spam the console with this
@@ -128,6 +168,40 @@ exports.moveStaticPages = async ({ netlifyConfig, target, i18n }) => {
128168
The following files matched middleware and were not moved to the CDN:
129169
130170
${yellowBright([...matchedPages].map((mid) => `- ${mid}`).join('\n'))}
171+
172+
────────────────────────────────────────────────────────────────
173+
`,
174+
)
175+
}
176+
}
177+
178+
if (matchedRedirects.size !== 0 || matchedRewrites.size !== 0) {
179+
console.log(
180+
yellowBright(outdent`
181+
Skipped moving ${
182+
matchedRedirects.size + matchedRewrites.size
183+
} files because they match redirects or beforeFiles rewrites, so cannot be deployed to the CDN and will be served from the origin instead.
184+
`),
185+
)
186+
if (matchedRedirects.size < 50 && matchedRedirects.size !== 0) {
187+
console.log(
188+
outdent`
189+
The following files matched redirects and were not moved to the CDN:
190+
191+
${yellowBright([...matchedRedirects].map((mid) => `- ${mid}`).join('\n'))}
192+
193+
────────────────────────────────────────────────────────────────
194+
`,
195+
)
196+
}
197+
if (matchedRewrites.size < 50 && matchedRewrites.size !== 0) {
198+
console.log(
199+
outdent`
200+
The following files matched beforeFiles rewrites and were not moved to the CDN:
201+
202+
${yellowBright([...matchedRewrites].map((mid) => `- ${mid}`).join('\n'))}
203+
204+
────────────────────────────────────────────────────────────────
131205
`,
132206
)
133207
}
@@ -151,3 +225,4 @@ exports.movePublicFiles = async ({ appDir, publish }) => {
151225
await copy(publicDir, `${publish}/`)
152226
}
153227
}
228+
/* eslint-enable max-lines */

test/index.js

+80-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const plugin = require('../src')
1010

1111
const { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME } = require('../src/constants')
1212
const { join } = require('pathe')
13-
const { matchMiddleware, stripLocale } = require('../src/helpers/files')
13+
const { matchMiddleware, stripLocale, matchesRedirect, matchesRewrite } = require('../src/helpers/files')
1414

1515
const FIXTURES_DIR = `${__dirname}/fixtures`
1616
const SAMPLE_PROJECT_DIR = `${__dirname}/../demo`
@@ -32,6 +32,45 @@ const utils = {
3232
},
3333
}
3434

35+
const REDIRECTS = [
36+
{
37+
source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/',
38+
destination: '/:file',
39+
locale: false,
40+
internal: true,
41+
statusCode: 308,
42+
regex: '^(?:/((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+))/$',
43+
},
44+
{
45+
source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)',
46+
destination: '/:notfile/',
47+
locale: false,
48+
internal: true,
49+
statusCode: 308,
50+
regex: '^(?:/((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+))$',
51+
},
52+
{
53+
source: '/en/redirectme',
54+
destination: '/',
55+
statusCode: 308,
56+
regex: '^(?!/_next)/en/redirectme(?:/)?$',
57+
},
58+
{
59+
source: '/:nextInternalLocale(en|es|fr)/redirectme',
60+
destination: '/:nextInternalLocale/',
61+
statusCode: 308,
62+
regex: '^(?!/_next)(?:/(en|es|fr))/redirectme(?:/)?$',
63+
},
64+
]
65+
66+
const REWRITES = [
67+
{
68+
source: '/:nextInternalLocale(en|es|fr)/old/:path*',
69+
destination: '/:nextInternalLocale/:path*',
70+
regex: '^(?:/(en|es|fr))/old(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?(?:/)?$',
71+
},
72+
]
73+
3574
// Temporary switch cwd
3675
const changeCwd = function (cwd) {
3776
const originalCwd = process.cwd()
@@ -450,6 +489,46 @@ describe('utility functions', () => {
450489
expect(stripLocale(path, locales)).toEqual(path)
451490
}
452491
})
492+
493+
test('matchesRedirect correctly matches paths with locales', () => {
494+
const paths = ['en/redirectme.html', 'en/redirectme.json', 'fr/redirectme.html', 'fr/redirectme.json']
495+
paths.forEach((path) => {
496+
expect(matchesRedirect(path, REDIRECTS)).toBeTruthy()
497+
})
498+
})
499+
500+
test("matchesRedirect doesn't match paths with invalid locales", () => {
501+
const paths = ['dk/redirectme.html', 'dk/redirectme.json', 'gr/redirectme.html', 'gr/redirectme.json']
502+
paths.forEach((path) => {
503+
expect(matchesRedirect(path, REDIRECTS)).toBeFalsy()
504+
})
505+
})
506+
507+
test("matchesRedirect doesn't match internal redirects", () => {
508+
const paths = ['en/notrailingslash']
509+
paths.forEach((path) => {
510+
expect(matchesRedirect(path, REDIRECTS)).toBeFalsy()
511+
})
512+
})
513+
514+
it('matchesRewrite matches array of rewrites', () => {
515+
expect(matchesRewrite('en/old/page.html', REWRITES)).toBeTruthy()
516+
})
517+
518+
it('matchesRewrite matches beforeFiles rewrites', () => {
519+
expect(matchesRewrite('en/old/page.html', { beforeFiles: REWRITES })).toBeTruthy()
520+
})
521+
522+
it("matchesRewrite doesn't match afterFiles rewrites", () => {
523+
expect(matchesRewrite('en/old/page.html', { afterFiles: REWRITES })).toBeFalsy()
524+
})
525+
526+
it('matchesRewrite matches various paths', () => {
527+
const paths = ['en/old/page.html', 'fr/old/page.html', 'en/old/deep/page.html', 'en/old.html']
528+
paths.forEach((path) => {
529+
expect(matchesRewrite(path, REWRITES)).toBeTruthy()
530+
})
531+
})
453532
})
454533

455534
describe('function helpers', () => {

0 commit comments

Comments
 (0)