Skip to content

Commit

Permalink
fix(next/image): handle undefined images.localPatterns config in `i…
Browse files Browse the repository at this point in the history
…mages-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).
  • Loading branch information
styfle authored Oct 2, 2024
1 parent 1a81fe4 commit f365109
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 18 deletions.
16 changes: 11 additions & 5 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
21 changes: 14 additions & 7 deletions packages/next/src/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
assertNoRedbox,
fetchViaHTTP,
findPort,
getImagesManifest,
getRedboxHeader,
killApp,
launchApp,
Expand Down Expand Up @@ -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', () => {
Expand Down
34 changes: 33 additions & 1 deletion test/integration/next-image-new/app-dir/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
check,
fetchViaHTTP,
findPort,
getImagesManifest,
getRedboxHeader,
killApp,
launchApp,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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', () => {
Expand Down
42 changes: 37 additions & 5 deletions test/integration/next-image-new/unoptimized/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { join } from 'path'
import {
check,
findPort,
getImagesManifest,
killApp,
launchApp,
nextBuild,
Expand All @@ -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(
Expand Down Expand Up @@ -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', () => {
Expand All @@ -101,7 +133,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})

runTests('/')
runTests('/', 'dev')
})
;(process.env.TURBOPACK_DEV ? describe.skip : describe)(
'production mode - component',
Expand All @@ -115,7 +147,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})

runTests('/')
runTests('/', 'server')
}
)
describe('development mode - getImageProps', () => {
Expand All @@ -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',
Expand All @@ -141,7 +173,7 @@ describe('Unoptimized Image Tests', () => {
await killApp(app)
})

runTests('/get-img-props')
runTests('/get-img-props', 'server')
}
)
})
4 changes: 4 additions & 0 deletions test/lib/next-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit f365109

Please sign in to comment.