From e03c4ff4ff53d7ad94dde19c45ed2dd4d94d91f0 Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 4 Oct 2024 12:29:47 -0400 Subject: [PATCH] feat(next/image): add `images.localPatterns` config (#70802) - Backport https://github.com/vercel/next.js/pull/70529 to `14.2.x` --- .../06-optimizing/01-images.mdx | 15 +++ .../02-api-reference/01-components/image.mdx | 19 ++++ errors/invalid-images-config.mdx | 2 + .../next-image-unconfigured-localpatterns.mdx | 29 +++++ packages/next/src/build/index.ts | 29 ++--- .../webpack/plugins/define-env-plugin.ts | 5 +- packages/next/src/client/legacy/image.tsx | 25 ++++- packages/next/src/server/config-schema.ts | 9 ++ packages/next/src/server/config.ts | 13 +++ packages/next/src/server/image-optimizer.ts | 9 +- packages/next/src/shared/lib/image-config.ts | 21 +++- packages/next/src/shared/lib/image-loader.ts | 21 +++- .../src/shared/lib/match-local-pattern.ts | 29 +++++ .../src/shared/lib/match-remote-pattern.ts | 2 +- packages/next/src/telemetry/events/version.ts | 5 + .../image-optimizer/test/index.test.ts | 52 +++++++++ .../app/does-not-exist/page.js | 17 +++ .../app/images/static-img.png | Bin 0 -> 1545 bytes .../app-dir-localpatterns/app/layout.js | 12 +++ .../app/nested-assets-query/page.js | 17 +++ .../app/nested-blocked/page.js | 17 +++ .../app-dir-localpatterns/app/page.js | 26 +++++ .../app/top-level/page.js | 17 +++ .../app-dir-localpatterns/next.config.js | 10 ++ .../public/assets/test.png | Bin 0 -> 1545 bytes .../public/blocked/test.png | Bin 0 -> 1545 bytes .../app-dir-localpatterns/public/test.png | Bin 0 -> 1545 bytes .../app-dir-localpatterns/style.module.css | 18 ++++ .../app-dir-localpatterns/test/index.test.ts | 99 ++++++++++++++++++ .../telemetry/next.config.i18n-images | 1 + .../integration/telemetry/test/config.test.js | 2 + .../match-local-pattern.test.ts | 98 +++++++++++++++++ .../match-remote-pattern.test.ts | 2 +- 33 files changed, 598 insertions(+), 23 deletions(-) create mode 100644 errors/next-image-unconfigured-localpatterns.mdx create mode 100644 packages/next/src/shared/lib/match-local-pattern.ts create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/does-not-exist/page.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/images/static-img.png create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/layout.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/nested-assets-query/page.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/nested-blocked/page.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/page.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/app/top-level/page.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/next.config.js create mode 100644 test/integration/next-image-new/app-dir-localpatterns/public/assets/test.png create mode 100644 test/integration/next-image-new/app-dir-localpatterns/public/blocked/test.png create mode 100644 test/integration/next-image-new/app-dir-localpatterns/public/test.png create mode 100644 test/integration/next-image-new/app-dir-localpatterns/style.module.css create mode 100644 test/integration/next-image-new/app-dir-localpatterns/test/index.test.ts create mode 100644 test/unit/image-optimizer/match-local-pattern.test.ts diff --git a/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx b/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx index 76f210fbb17b2..68ac3e282661f 100644 --- a/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx +++ b/docs/02-app/01-building-your-application/06-optimizing/01-images.mdx @@ -89,6 +89,21 @@ export default function Page() { > **Warning:** Dynamic `await import()` or `require()` are _not_ supported. The `import` must be static so it can be analyzed at build time. +You can optionally configure `localPatterns` in your `next.config.js` file in order to allow specific images and block all others. + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/images/**', + search: '', + }, + ], + }, +} +``` + ### Remote Images To use a remote image, the `src` property should be a URL string. diff --git a/docs/02-app/02-api-reference/01-components/image.mdx b/docs/02-app/02-api-reference/01-components/image.mdx index 440f95b627c05..f3f1245aed000 100644 --- a/docs/02-app/02-api-reference/01-components/image.mdx +++ b/docs/02-app/02-api-reference/01-components/image.mdx @@ -484,6 +484,25 @@ Other properties on the `` component will be passed to the underlying In addition to props, you can configure the Image Component in `next.config.js`. The following options are available: +## `localPatterns` + +You can optionally configure `localPatterns` in your `next.config.js` file in order to allow specific paths to be optimized and block all others paths. + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/images/**', + search: '', + }, + ], + }, +} +``` + +> **Good to know**: The example above will ensure the `src` property of `next/image` must start with `/assets/images/` and must not have a query string. Attempting to optimize any other path will respond with 400 Bad Request. + ### `remotePatterns` To protect your application from malicious users, configuration is required in order to use external images. This ensures that only external images from your account can be served from the Next.js Image Optimization API. These external images can be configured with the `remotePatterns` property in your `next.config.js` file, as shown below: diff --git a/errors/invalid-images-config.mdx b/errors/invalid-images-config.mdx index 4f14b668eb3a0..cbeca3c873891 100644 --- a/errors/invalid-images-config.mdx +++ b/errors/invalid-images-config.mdx @@ -37,6 +37,8 @@ module.exports = { contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", // sets the Content-Disposition header (inline or attachment) contentDispositionType: 'inline', + // limit of 25 objects + localPatterns: [], // limit of 50 objects remotePatterns: [], // when true, every image will be unoptimized diff --git a/errors/next-image-unconfigured-localpatterns.mdx b/errors/next-image-unconfigured-localpatterns.mdx new file mode 100644 index 0000000000000..34a9a19d5e34f --- /dev/null +++ b/errors/next-image-unconfigured-localpatterns.mdx @@ -0,0 +1,29 @@ +--- +title: '`next/image` Un-configured localPatterns' +--- + +## Why This Error Occurred + +One of your pages that leverages the `next/image` component, passed a `src` value that uses a URL that isn't defined in the `images.localPatterns` property in `next.config.js`. + +## Possible Ways to Fix It + +Add an entry to `images.localPatterns` array in `next.config.js` with the expected URL pattern. For example: + +```js filename="next.config.js" +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/**', + search: '', + }, + ], + }, +} +``` + +## Useful Links + +- [Image Optimization Documentation](/docs/pages/building-your-application/optimizing/images) +- [Local Patterns Documentation](/docs/pages/api-reference/components/image#localpatterns) diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index 6c09660781eb7..54da0dadbb727 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -420,6 +420,11 @@ 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, + })) await writeManifest(path.join(distDir, IMAGES_MANIFEST), { version: 1, @@ -1885,18 +1890,18 @@ export default async function build( config.experimental.gzipSize ) - const middlewareManifest: MiddlewareManifest = require(path.join( - distDir, - SERVER_DIRECTORY, - MIDDLEWARE_MANIFEST - )) + const middlewareManifest: MiddlewareManifest = require( + path.join(distDir, SERVER_DIRECTORY, MIDDLEWARE_MANIFEST) + ) const actionManifest = appDir - ? (require(path.join( - distDir, - SERVER_DIRECTORY, - SERVER_REFERENCE_MANIFEST + '.json' - )) as ActionManifest) + ? (require( + path.join( + distDir, + SERVER_DIRECTORY, + SERVER_REFERENCE_MANIFEST + '.json' + ) + ) as ActionManifest) : null const entriesWithAction = actionManifest ? new Set() : null if (actionManifest && entriesWithAction) { @@ -3282,8 +3287,8 @@ export default async function build( fallback: ssgBlockingFallbackPages.has(tbdRoute) ? null : ssgStaticFallbackPages.has(tbdRoute) - ? `${normalizedRoute}.html` - : false, + ? `${normalizedRoute}.html` + : false, dataRouteRegex: normalizeRouteRegex( getNamedRouteRegex( dataRoute.replace(/\.json$/, ''), diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index a7748f74821f1..c6f38c90d42b8 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -117,6 +117,7 @@ function getImageConfig( // pass domains in development to allow validating on the client domains: config.images.domains, remotePatterns: config.images?.remotePatterns, + localPatterns: config.images?.localPatterns, output: config.output, } : {}), @@ -162,8 +163,8 @@ export function getDefineEnv({ 'process.env.NEXT_RUNTIME': isEdgeServer ? 'edge' : isNodeServer - ? 'nodejs' - : '', + ? 'nodejs' + : '', 'process.env.NEXT_MINIMAL': '', 'process.env.__NEXT_PPR': config.experimental.ppr === true, 'process.env.NEXT_DEPLOYMENT_ID': config.deploymentId || false, diff --git a/packages/next/src/client/legacy/image.tsx b/packages/next/src/client/legacy/image.tsx index d982e9fa8a330..8900cbb2f0ea5 100644 --- a/packages/next/src/client/legacy/image.tsx +++ b/packages/next/src/client/legacy/image.tsx @@ -139,6 +139,25 @@ function defaultLoader({ ) } + if (src.startsWith('/') && config.localPatterns) { + if ( + process.env.NODE_ENV !== 'test' && + // micromatch isn't compatible with edge runtime + process.env.NEXT_RUNTIME !== 'edge' + ) { + // We use dynamic require because this should only error in development + const { + hasLocalMatch, + } = require('../../shared/lib/match-local-pattern') + if (!hasLocalMatch(config.localPatterns, src)) { + throw new Error( + `Invalid src prop (${src}) on \`next/image\` does not match \`images.localPatterns\` configured in your \`next.config.js\`\n` + + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` + ) + } + } + } + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { let parsedSrc: URL try { @@ -156,8 +175,10 @@ function defaultLoader({ process.env.NEXT_RUNTIME !== 'edge' ) { // We use dynamic require because this should only error in development - const { hasMatch } = require('../../shared/lib/match-remote-pattern') - if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) { + const { + hasRemoteMatch, + } = require('../../shared/lib/match-remote-pattern') + if (!hasRemoteMatch(config.domains, config.remotePatterns, parsedSrc)) { throw new Error( `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index c04422bc0a8e2..fb36445859114 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -461,6 +461,15 @@ export const configSchema: zod.ZodType = z.lazy(() => .optional(), images: z .strictObject({ + localPatterns: z + .array( + z.strictObject({ + pathname: z.string().optional(), + search: z.string().optional(), + }) + ) + .max(25) + .optional(), remotePatterns: z .array( z.strictObject({ diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 859d834374eb1..08cc124945e5c 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -368,6 +368,19 @@ function assignDefaults( ) } + if (images.localPatterns) { + if (!Array.isArray(images.localPatterns)) { + throw new Error( + `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: '', + }) + } + if (images.remotePatterns) { if (!Array.isArray(images.remotePatterns)) { throw new Error( diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 34ea5e9c9ff5e..79b98009bac52 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -12,7 +12,8 @@ import nodeUrl, { type UrlWithParsedQuery } from 'url' import { getImageBlurSvg } from '../shared/lib/image-blur-svg' import type { ImageConfigComplete } from '../shared/lib/image-config' -import { hasMatch } from '../shared/lib/match-remote-pattern' +import { hasLocalMatch } from '../shared/lib/match-local-pattern' +import { hasRemoteMatch } from '../shared/lib/match-remote-pattern' import type { NextConfigComplete } from './config-shared' import { createRequestResponseMocks } from './lib/mock-request' // Do not import anything other than types from this module @@ -173,6 +174,7 @@ export class ImageOptimizerCache { formats = ['image/webp'], } = imageData const remotePatterns = nextConfig.images?.remotePatterns || [] + const localPatterns = nextConfig.images?.localPatterns const { url, w, q } = query let href: string @@ -212,6 +214,9 @@ export class ImageOptimizerCache { errorMessage: '"url" parameter cannot be recursive', } } + if (!hasLocalMatch(localPatterns, url)) { + return { errorMessage: '"url" parameter is not allowed' } + } } else { let hrefParsed: URL @@ -227,7 +232,7 @@ export class ImageOptimizerCache { return { errorMessage: '"url" parameter is invalid' } } - if (!hasMatch(domains, remotePatterns, hrefParsed)) { + if (!hasRemoteMatch(domains, remotePatterns, hrefParsed)) { return { errorMessage: '"url" parameter is not allowed' } } } diff --git a/packages/next/src/shared/lib/image-config.ts b/packages/next/src/shared/lib/image-config.ts index 66095a84120ea..387dbdf19000b 100644 --- a/packages/next/src/shared/lib/image-config.ts +++ b/packages/next/src/shared/lib/image-config.ts @@ -18,6 +18,21 @@ export type ImageLoaderPropsWithConfig = ImageLoaderProps & { config: Readonly } +export type LocalPattern = { + /** + * Can be literal or wildcard. + * Single `*` matches a single path segment. + * Double `**` matches any number of path segments. + */ + pathname?: string + + /** + * Can be literal query string such as `?v=1` or + * empty string meaning no query string. + */ + search?: string +} + export type RemotePattern = { /** * Must be `http` or `https`. @@ -100,6 +115,9 @@ export type ImageConfigComplete = { /** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#remotepatterns) */ remotePatterns: RemotePattern[] + /** @see [Remote Patterns](https://nextjs.org/docs/api-reference/next/image#localPatterns) */ + localPatterns: LocalPattern[] | undefined + /** @see [Unoptimized](https://nextjs.org/docs/api-reference/next/image#unoptimized) */ unoptimized: boolean } @@ -119,6 +137,7 @@ export const imageConfigDefault: ImageConfigComplete = { dangerouslyAllowSVG: false, contentSecurityPolicy: `script-src 'none'; frame-src 'none'; sandbox;`, contentDispositionType: 'inline', - remotePatterns: [], + localPatterns: undefined, // default: allow all local images + remotePatterns: [], // default: allow no remote images unoptimized: false, } diff --git a/packages/next/src/shared/lib/image-loader.ts b/packages/next/src/shared/lib/image-loader.ts index 43dac3645f484..f44721dec9859 100644 --- a/packages/next/src/shared/lib/image-loader.ts +++ b/packages/next/src/shared/lib/image-loader.ts @@ -29,6 +29,23 @@ function defaultLoader({ ) } + if (src.startsWith('/') && config.localPatterns) { + if ( + process.env.NODE_ENV !== 'test' && + // micromatch isn't compatible with edge runtime + process.env.NEXT_RUNTIME !== 'edge' + ) { + // We use dynamic require because this should only error in development + const { hasLocalMatch } = require('./match-local-pattern') + if (!hasLocalMatch(config.localPatterns, src)) { + throw new Error( + `Invalid src prop (${src}) on \`next/image\` does not match \`images.localPatterns\` configured in your \`next.config.js\`\n` + + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-localpatterns` + ) + } + } + } + if (!src.startsWith('/') && (config.domains || config.remotePatterns)) { let parsedSrc: URL try { @@ -46,8 +63,8 @@ function defaultLoader({ process.env.NEXT_RUNTIME !== 'edge' ) { // We use dynamic require because this should only error in development - const { hasMatch } = require('./match-remote-pattern') - if (!hasMatch(config.domains, config.remotePatterns, parsedSrc)) { + const { hasRemoteMatch } = require('./match-remote-pattern') + if (!hasRemoteMatch(config.domains, config.remotePatterns, parsedSrc)) { throw new Error( `Invalid src prop (${src}) on \`next/image\`, hostname "${parsedSrc.hostname}" is not configured under images in your \`next.config.js\`\n` + `See more info: https://nextjs.org/docs/messages/next-image-unconfigured-host` diff --git a/packages/next/src/shared/lib/match-local-pattern.ts b/packages/next/src/shared/lib/match-local-pattern.ts new file mode 100644 index 0000000000000..bad5f26de25d1 --- /dev/null +++ b/packages/next/src/shared/lib/match-local-pattern.ts @@ -0,0 +1,29 @@ +import type { LocalPattern } from './image-config' +import { makeRe } from 'next/dist/compiled/picomatch' + +// Modifying this function should also modify writeImagesManifest() +export function matchLocalPattern(pattern: LocalPattern, url: URL): boolean { + if (pattern.search !== undefined) { + if (pattern.search !== url.search) { + return false + } + } + + if (!makeRe(pattern.pathname ?? '**', { dot: true }).test(url.pathname)) { + return false + } + + return true +} + +export function hasLocalMatch( + localPatterns: LocalPattern[] | undefined, + urlPathAndQuery: string +): boolean { + if (!localPatterns) { + // if the user didn't define "localPatterns", we allow all local images + return true + } + const url = new URL(urlPathAndQuery, 'http://n') + return localPatterns.some((p) => matchLocalPattern(p, url)) +} diff --git a/packages/next/src/shared/lib/match-remote-pattern.ts b/packages/next/src/shared/lib/match-remote-pattern.ts index 020f596006ffc..51a40f2658c36 100644 --- a/packages/next/src/shared/lib/match-remote-pattern.ts +++ b/packages/next/src/shared/lib/match-remote-pattern.ts @@ -39,7 +39,7 @@ export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { return true } -export function hasMatch( +export function hasRemoteMatch( domains: string[], remotePatterns: RemotePattern[], url: URL diff --git a/packages/next/src/telemetry/events/version.ts b/packages/next/src/telemetry/events/version.ts index f70cf3db48cd1..d50edd5087085 100644 --- a/packages/next/src/telemetry/events/version.ts +++ b/packages/next/src/telemetry/events/version.ts @@ -23,6 +23,7 @@ type EventCliSessionStarted = { localeDetectionEnabled: boolean | null imageDomainsCount: number | null imageRemotePatternsCount: number | null + imageLocalPatternsCount: number | null imageSizes: string | null imageLoader: string | null imageFormats: string | null @@ -75,6 +76,7 @@ export function eventCliSession( | 'localeDetectionEnabled' | 'imageDomainsCount' | 'imageRemotePatternsCount' + | 'imageLocalPatternsCount' | 'imageSizes' | 'imageLoader' | 'imageFormats' @@ -114,6 +116,9 @@ export function eventCliSession( imageRemotePatternsCount: images?.remotePatterns ? images.remotePatterns.length : null, + imageLocalPatternsCount: images?.localPatterns + ? images.localPatterns.length + : null, imageSizes: images?.imageSizes ? images.imageSizes.join(',') : null, imageLoader: images?.loader, imageFormats: images?.formats ? images.formats.join(',') : null, diff --git a/test/integration/image-optimizer/test/index.test.ts b/test/integration/image-optimizer/test/index.test.ts index 0148facf74e13..87eb7b29cace4 100644 --- a/test/integration/image-optimizer/test/index.test.ts +++ b/test/integration/image-optimizer/test/index.test.ts @@ -48,6 +48,58 @@ describe('Image Optimizer', () => { ) }) + it('should error when localPatterns length exceeds 25', async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + localPatterns: Array.from({ length: 26 }).map((_) => ({ + pathname: '/foo/**', + })), + }, + }) + ) + let stderr = '' + + app = await launchApp(appDir, await findPort(), { + onStderr(msg) { + stderr += msg || '' + }, + }) + await waitFor(1000) + await killApp(app).catch(() => {}) + await nextConfig.restore() + + expect(stderr).toContain( + 'Array must contain at most 25 element(s) at "images.localPatterns"' + ) + }) + + it('should error when localPatterns has invalid prop', async () => { + await nextConfig.replace( + '{ /* replaceme */ }', + JSON.stringify({ + images: { + localPatterns: [{ pathname: '/foo/**', foo: 'bar' }], + }, + }) + ) + let stderr = '' + + app = await launchApp(appDir, await findPort(), { + onStderr(msg) { + stderr += msg || '' + }, + }) + await waitFor(1000) + await killApp(app).catch(() => {}) + await nextConfig.restore() + + expect(stderr).toContain( + `Unrecognized key(s) in object: 'foo' at "images.localPatterns[0]"` + ) + }) + it('should error when remotePatterns length exceeds 50', async () => { await nextConfig.replace( '{ /* replaceme */ }', diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/does-not-exist/page.js b/test/integration/next-image-new/app-dir-localpatterns/app/does-not-exist/page.js new file mode 100644 index 0000000000000..0cfeeeaa6431c --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/does-not-exist/page.js @@ -0,0 +1,17 @@ +import Image from 'next/image' + +const Page = () => { + return ( +
+ should fail +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/images/static-img.png b/test/integration/next-image-new/app-dir-localpatterns/app/images/static-img.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/layout.js b/test/integration/next-image-new/app-dir-localpatterns/app/layout.js new file mode 100644 index 0000000000000..8525f5f8c0b2a --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/nested-assets-query/page.js b/test/integration/next-image-new/app-dir-localpatterns/app/nested-assets-query/page.js new file mode 100644 index 0000000000000..ce9b1849f7288 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/nested-assets-query/page.js @@ -0,0 +1,17 @@ +import Image from 'next/image' + +const Page = () => { + return ( +
+ should fail +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/nested-blocked/page.js b/test/integration/next-image-new/app-dir-localpatterns/app/nested-blocked/page.js new file mode 100644 index 0000000000000..16a46a777e626 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/nested-blocked/page.js @@ -0,0 +1,17 @@ +import Image from 'next/image' + +const Page = () => { + return ( +
+ should fail +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/page.js b/test/integration/next-image-new/app-dir-localpatterns/app/page.js new file mode 100644 index 0000000000000..a435846385864 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/page.js @@ -0,0 +1,26 @@ +import Image from 'next/image' + +import staticImg from './images/static-img.png' + +const Page = () => { + return ( +
+ should work + should work +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/app-dir-localpatterns/app/top-level/page.js b/test/integration/next-image-new/app-dir-localpatterns/app/top-level/page.js new file mode 100644 index 0000000000000..ccef79fcfb781 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/app/top-level/page.js @@ -0,0 +1,17 @@ +import Image from 'next/image' + +const Page = () => { + return ( +
+ should fail +
+ ) +} + +export default Page diff --git a/test/integration/next-image-new/app-dir-localpatterns/next.config.js b/test/integration/next-image-new/app-dir-localpatterns/next.config.js new file mode 100644 index 0000000000000..10c28b1a185c0 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/next.config.js @@ -0,0 +1,10 @@ +module.exports = { + images: { + localPatterns: [ + { + pathname: '/assets/**', + search: '', + }, + ], + }, +} diff --git a/test/integration/next-image-new/app-dir-localpatterns/public/assets/test.png b/test/integration/next-image-new/app-dir-localpatterns/public/assets/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/next-image-new/app-dir-localpatterns/public/blocked/test.png b/test/integration/next-image-new/app-dir-localpatterns/public/blocked/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/next-image-new/app-dir-localpatterns/public/test.png b/test/integration/next-image-new/app-dir-localpatterns/public/test.png new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/next-image-new/app-dir-localpatterns/style.module.css b/test/integration/next-image-new/app-dir-localpatterns/style.module.css new file mode 100644 index 0000000000000..e538759372d08 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/style.module.css @@ -0,0 +1,18 @@ +.displayFlex { + display: flex; +} + +.mainContainer span { + margin: 57px; +} + +.mainContainer img { + border-radius: 139px; +} + +.overrideImg { + filter: opacity(0.5); + background-size: 30%; + background-image: url('data:image/png;base64,iVBORw0KGgo='); + background-position: 1px 2px; +} diff --git a/test/integration/next-image-new/app-dir-localpatterns/test/index.test.ts b/test/integration/next-image-new/app-dir-localpatterns/test/index.test.ts new file mode 100644 index 0000000000000..e95edf3ebd2d4 --- /dev/null +++ b/test/integration/next-image-new/app-dir-localpatterns/test/index.test.ts @@ -0,0 +1,99 @@ +/* eslint-env jest */ + +import { + assertHasRedbox, + assertNoRedbox, + fetchViaHTTP, + findPort, + getRedboxHeader, + killApp, + launchApp, + nextBuild, + nextStart, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +import { join } from 'path' + +const appDir = join(__dirname, '../') + +let appPort: number +let app: Awaited> + +async function getSrc( + browser: Awaited>, + id: string +) { + const src = await browser.elementById(id).getAttribute('src') + if (src) { + const url = new URL(src, `http://localhost:${appPort}`) + return url.href.slice(url.origin.length) + } +} + +function runTests(mode: 'dev' | 'server') { + it('should load matching images', async () => { + const browser = await webdriver(appPort, '/') + if (mode === 'dev') { + await assertNoRedbox(browser) + } + const ids = ['nested-assets', 'static-img'] + const urls = await Promise.all(ids.map((id) => getSrc(browser, id))) + const responses = await Promise.all( + urls.map((url) => fetchViaHTTP(appPort, url)) + ) + const statuses = responses.map((res) => res.status) + expect(statuses).toStrictEqual([200, 200]) + }) + + it.each([ + 'does-not-exist', + 'nested-assets-query', + 'nested-blocked', + 'top-level', + ])('should block unmatched image %s', async (id: string) => { + const page = '/' + id + const browser = await webdriver(appPort, page) + if (mode === 'dev') { + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch( + /Invalid src prop (.+) on `next\/image` does not match `images.localPatterns` configured/g + ) + } else { + const url = await getSrc(browser, id) + const res = await fetchViaHTTP(appPort, url) + expect(res.status).toBe(400) + } + }) +} + +describe('Image localPatterns config', () => { + ;(process.env.TURBOPACK_BUILD ? describe.skip : describe)( + 'development mode', + () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + }) + + runTests('dev') + } + ) + ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( + 'production mode', + () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + }) + afterAll(async () => { + await killApp(app) + }) + + runTests('server') + } + ) +}) diff --git a/test/integration/telemetry/next.config.i18n-images b/test/integration/telemetry/next.config.i18n-images index 0a7be628c6de2..c9f3a5cb8a507 100644 --- a/test/integration/telemetry/next.config.i18n-images +++ b/test/integration/telemetry/next.config.i18n-images @@ -5,6 +5,7 @@ module.exports = phase => { imageSizes: [64, 128, 256, 512, 1024], domains: ['example.com', 'another.com'], remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }], + localPatterns: [{ pathname: '/assets/**', search: '' }], }, i18n: { locales: ['en','nl','fr'], diff --git a/test/integration/telemetry/test/config.test.js b/test/integration/telemetry/test/config.test.js index a07615c764d54..9379d954abd08 100644 --- a/test/integration/telemetry/test/config.test.js +++ b/test/integration/telemetry/test/config.test.js @@ -75,6 +75,7 @@ describe('config telemetry', () => { expect(event1).toMatch(/"imageFutureEnabled": true/) expect(event1).toMatch(/"imageDomainsCount": 2/) expect(event1).toMatch(/"imageRemotePatternsCount": 1/) + expect(event1).toMatch(/"imageLocalPatternsCount": 2/) expect(event1).toMatch(/"imageSizes": "64,128,256,512,1024"/) expect(event1).toMatch(/"imageFormats": "image\/avif,image\/webp"/) expect(event1).toMatch(/"nextConfigOutput": null/) @@ -121,6 +122,7 @@ describe('config telemetry', () => { expect(event2).toMatch(/"localeDetectionEnabled": true/) expect(event2).toMatch(/"imageDomainsCount": 2/) expect(event2).toMatch(/"imageRemotePatternsCount": 1/) + expect(event2).toMatch(/"imageLocalPatternsCount": 2/) expect(event2).toMatch(/"imageSizes": "64,128,256,512,1024"/) expect(event2).toMatch(/"nextConfigOutput": null/) expect(event2).toMatch(/"trailingSlashEnabled": false/) diff --git a/test/unit/image-optimizer/match-local-pattern.test.ts b/test/unit/image-optimizer/match-local-pattern.test.ts new file mode 100644 index 0000000000000..5f911b47174ea --- /dev/null +++ b/test/unit/image-optimizer/match-local-pattern.test.ts @@ -0,0 +1,98 @@ +/* eslint-env jest */ +import type { LocalPattern } from 'next/dist/shared/lib/image-config' +import { + matchLocalPattern, + hasLocalMatch as hasMatch, +} from 'next/dist/shared/lib/match-local-pattern' + +const m = (p: LocalPattern, urlPathAndQuery: string) => + matchLocalPattern(p, new URL(urlPathAndQuery, 'http://n')) + +describe('matchLocalPattern', () => { + it('should match anything when no pattern is defined', () => { + const p = {} as const + expect(m(p, '/')).toBe(true) + expect(m(p, '/path')).toBe(true) + expect(m(p, '/path/to')).toBe(true) + expect(m(p, '/path/to/file')).toBe(true) + expect(m(p, '/path/to/file.txt')).toBe(true) + expect(m(p, '/path/to/file?q=1')).toBe(true) + expect(m(p, '/path/to/file?q=1&a=two')).toBe(true) + }) + + it('should match any path without a search query string', () => { + const p = { + search: '', + } as const + expect(m(p, '/')).toBe(true) + expect(m(p, '/path')).toBe(true) + expect(m(p, '/path/to')).toBe(true) + expect(m(p, '/path/to/file')).toBe(true) + expect(m(p, '/path/to/file.txt')).toBe(true) + expect(m(p, '/path/to/file?q=1')).toBe(false) + expect(m(p, '/path/to/file?q=1&a=two')).toBe(false) + expect(m(p, '/path/to/file.txt?q=1&a=two')).toBe(false) + }) + + it('should match literal pathname and any search query string', () => { + const p = { + pathname: '/path/to/file', + } as const + expect(m(p, '/')).toBe(false) + expect(m(p, '/path')).toBe(false) + expect(m(p, '/path/to')).toBe(false) + expect(m(p, '/path/to/file')).toBe(true) + expect(m(p, '/path/to/file.txt')).toBe(false) + expect(m(p, '/path/to/file?q=1')).toBe(true) + expect(m(p, '/path/to/file?q=1&a=two')).toBe(true) + expect(m(p, '/path/to/file.txt?q=1&a=two')).toBe(false) + }) + + it('should match pathname with double asterisk', () => { + const p = { + pathname: '/path/to/**', + } as const + expect(m(p, '/')).toBe(false) + expect(m(p, '/path')).toBe(false) + expect(m(p, '/path/to')).toBe(true) + expect(m(p, '/path/to/file')).toBe(true) + expect(m(p, '/path/to/file.txt')).toBe(true) + expect(m(p, '/path/to/file?q=1')).toBe(true) + expect(m(p, '/path/to/file?q=1&a=two')).toBe(true) + expect(m(p, '/path/to/file.txt?q=1&a=two')).toBe(true) + }) + + it('should properly work with hasMatch', () => { + const url = '/path/to/file?q=1&a=two' + expect(hasMatch(undefined, url)).toBe(true) + expect(hasMatch([], url)).toBe(false) + expect(hasMatch([{ pathname: '/path' }], url)).toBe(false) + expect(hasMatch([{ pathname: '/path/to' }], url)).toBe(false) + expect(hasMatch([{ pathname: '/path/to/file' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/to/file' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/to/file', search: '' }], url)).toBe( + false + ) + expect(hasMatch([{ pathname: '/path/to/file', search: '?q=1' }], url)).toBe( + false + ) + expect( + hasMatch([{ pathname: '/path/to/file', search: '?q=1&a=two' }], url) + ).toBe(true) + expect(hasMatch([{ pathname: '/path/**' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/to/**' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/to/f*' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/to/*le' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/path/*/file' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/*/to/file' }], url)).toBe(true) + expect(hasMatch([{ pathname: '/foo' }, { pathname: '/bar' }], url)).toBe( + false + ) + expect( + hasMatch( + [{ pathname: '/foo' }, { pathname: '/bar' }, { pathname: '/path/**' }], + url + ) + ).toBe(true) + }) +}) diff --git a/test/unit/image-optimizer/match-remote-pattern.test.ts b/test/unit/image-optimizer/match-remote-pattern.test.ts index d8ddc674c4a2c..27c2611d5f7a3 100644 --- a/test/unit/image-optimizer/match-remote-pattern.test.ts +++ b/test/unit/image-optimizer/match-remote-pattern.test.ts @@ -1,7 +1,7 @@ /* eslint-env jest */ import { matchRemotePattern as m, - hasMatch, + hasRemoteMatch as hasMatch, } from 'next/dist/shared/lib/match-remote-pattern' describe('matchRemotePattern', () => {