fix(next/image): handle undefined images.localPatterns config in images-manifest.json (#70730)

Since `images.localPatterns` config can be undefined, we need to support
that when emitting `images-manifest.json`.

- When `undefined`, all local images are allowed
- When `[]`, no local images are allowed

Note this is different than `remotePatterns` since it treats `undefined`
and `[]` the same (the default being no remote images are allowed).
This commit is contained in:
Steven
2024-10-02 22:13:58 +00:00
committed by GitHub
parent 1a81fe4408
commit f365109144
6 changed files with 142 additions and 18 deletions
+11 -5
View File
@@ -501,6 +501,8 @@ async function writeImagesManifest(
const images = { ...config.images }
const { deviceSizes, imageSizes } = images
;(images as any).sizes = [...deviceSizes, ...imageSizes]
// By default, remotePatterns will allow no remote images ([])
images.remotePatterns = (config?.images?.remotePatterns || []).map((p) => ({
// Modifying the manifest should also modify matchRemotePattern()
protocol: p.protocol,
@@ -509,11 +511,15 @@ async function writeImagesManifest(
pathname: makeRe(p.pathname ?? '**', { dot: true }).source,
search: p.search,
}))
images.localPatterns = (config?.images?.localPatterns || []).map((p) => ({
// Modifying the manifest should also modify matchLocalPattern()
pathname: makeRe(p.pathname ?? '**', { dot: true }).source,
search: p.search,
}))
// By default, localPatterns will allow all local images (undefined)
if (config?.images?.localPatterns) {
images.localPatterns = config.images.localPatterns.map((p) => ({
// Modifying the manifest should also modify matchLocalPattern()
pathname: makeRe(p.pathname ?? '**', { dot: true }).source,
search: p.search,
}))
}
await writeManifest(path.join(distDir, IMAGES_MANIFEST), {
version: 1,
+14 -7
View File
@@ -333,11 +333,18 @@ function assignDefaults(
`Specified images.localPatterns should be an Array received ${typeof images.localPatterns}.\nSee more info here: https://nextjs.org/docs/messages/invalid-images-config`
)
}
// static import images are automatically allowed
images.localPatterns.push({
pathname: '/_next/static/media/**',
search: '',
})
// avoid double-pushing the same pattern if it already exists
const hasMatch = images.localPatterns.some(
(pattern) =>
pattern.pathname === '/_next/static/media/**' && pattern.search === ''
)
if (!hasMatch) {
// static import images are automatically allowed
images.localPatterns.push({
pathname: '/_next/static/media/**',
search: '',
})
}
}
if (images.remotePatterns) {
@@ -357,9 +364,9 @@ function assignDefaults(
matchRemotePattern(pattern, url)
)
// avoid double-pushing the same remote if it already can be matched
// avoid double-pushing the same pattern if it already can be matched
if (!hasMatchForAssetPrefix) {
images.remotePatterns?.push({
images.remotePatterns.push({
hostname: url.hostname,
protocol: url.protocol.replace(/:$/, '') as 'http' | 'https',
port: url.port,
@@ -5,6 +5,7 @@ import {
assertNoRedbox,
fetchViaHTTP,
findPort,
getImagesManifest,
getRedboxHeader,
killApp,
launchApp,
@@ -64,6 +65,48 @@ function runTests(mode: 'dev' | 'server') {
expect(res.status).toBe(400)
}
})
if (mode === 'server') {
it('should build correct images-manifest.json', async () => {
const manifest = getImagesManifest(appDir)
expect(manifest).toEqual({
version: 1,
images: {
contentDispositionType: 'attachment',
contentSecurityPolicy:
"script-src 'none'; frame-src 'none'; sandbox;",
dangerouslyAllowSVG: false,
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
disableStaticImages: false,
domains: [],
formats: ['image/webp'],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
loader: 'default',
loaderFile: '',
remotePatterns: [],
localPatterns: [
{
pathname:
'^(?:\\/assets(?:\\/(?!\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)|$))$',
search: '',
},
{
pathname:
'^(?:\\/_next\\/static\\/media(?:\\/(?!\\.{1,2}(?:\\/|$))(?:(?:(?!(?:^|\\/)\\.{1,2}(?:\\/|$)).)*?)|$))$',
search: '',
},
],
minimumCacheTTL: 60,
path: '/_next/image',
sizes: [
640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96,
128, 256, 384,
],
unoptimized: false,
},
})
})
}
}
describe('Image localPatterns config', () => {
@@ -8,6 +8,7 @@ import {
check,
fetchViaHTTP,
findPort,
getImagesManifest,
getRedboxHeader,
killApp,
launchApp,
@@ -72,7 +73,7 @@ function getRatio(width, height) {
return height / width
}
function runTests(mode) {
function runTests(mode: 'dev' | 'server') {
it('should load the images', async () => {
let browser
try {
@@ -1588,6 +1589,37 @@ function runTests(mode) {
'callback refs that returned a cleanup should never be called with null'
)
})
if (mode === 'server') {
it('should build correct images-manifest.json', async () => {
const manifest = getImagesManifest(appDir)
expect(manifest).toEqual({
version: 1,
images: {
contentDispositionType: 'attachment',
contentSecurityPolicy:
"script-src 'none'; frame-src 'none'; sandbox;",
dangerouslyAllowSVG: false,
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
disableStaticImages: false,
domains: [],
formats: ['image/webp'],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
loader: 'default',
loaderFile: '',
remotePatterns: [],
localPatterns: undefined,
minimumCacheTTL: 60,
path: '/_next/image',
sizes: [
640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96,
128, 256, 384,
],
unoptimized: false,
},
})
})
}
}
describe('Image Component Default Tests', () => {
@@ -4,6 +4,7 @@ import { join } from 'path'
import {
check,
findPort,
getImagesManifest,
killApp,
launchApp,
nextBuild,
@@ -15,7 +16,7 @@ const appDir = join(__dirname, '../')
let appPort
let app
function runTests(url: string) {
function runTests(url: string, mode: 'dev' | 'server') {
it('should not optimize any image', async () => {
const browser = await webdriver(appPort, url)
expect(
@@ -89,6 +90,37 @@ function runTests(url: string) {
await browser.elementById('eager-image').getAttribute('srcset')
).toBeNull()
})
if (mode === 'server') {
it('should build correct images-manifest.json', async () => {
const manifest = getImagesManifest(appDir)
expect(manifest).toEqual({
version: 1,
images: {
contentDispositionType: 'attachment',
contentSecurityPolicy:
"script-src 'none'; frame-src 'none'; sandbox;",
dangerouslyAllowSVG: false,
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
disableStaticImages: false,
domains: [],
formats: ['image/webp'],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
loader: 'default',
loaderFile: '',
remotePatterns: [],
localPatterns: undefined,
minimumCacheTTL: 60,
path: '/_next/image',
sizes: [
640, 750, 828, 1080, 1200, 1920, 2048, 3840, 16, 32, 48, 64, 96,
128, 256, 384,
],
unoptimized: true,
},
})
})
}
}
describe('Unoptimized Image Tests', () => {
@@ -101,7 +133,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})
runTests('/')
runTests('/', 'dev')
})
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
'production mode - component',
@@ -115,7 +147,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})
runTests('/')
runTests('/', 'server')
}
)
describe('development mode - getImageProps', () => {
@@ -127,7 +159,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})
runTests('/get-img-props')
runTests('/get-img-props', 'dev')
})
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
'production mode - getImageProps',
@@ -141,7 +173,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})
runTests('/get-img-props')
runTests('/get-img-props', 'server')
}
)
})
+4
View File
@@ -1003,6 +1003,10 @@ export function getBuildManifest(dir: string) {
return readJson(path.join(dir, '.next/build-manifest.json'))
}
export function getImagesManifest(dir: string) {
return readJson(path.join(dir, '.next/images-manifest.json'))
}
export function getPageFilesFromBuildManifest(dir: string, page: string) {
const buildManifest = getBuildManifest(dir)
const pageFiles = buildManifest.pages[page]