···11+/* eslint-disable @typescript-eslint/no-explicit-any */
12import path from "node:path";
23import { fileURLToPath } from "node:url";
3445import { loadConfig } from "@opennextjs/aws/adapters/config/util.js";
56import type { BuildOptions } from "@opennextjs/aws/build/helper.js";
67import { build } from "esbuild";
88+import pm from "picomatch";
79810/**
911 * Compiles the initialization code for the workerd runtime
···1618 const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
1719 const basePath = nextConfig.basePath ?? "";
18202121+ // https://github.com/vercel/next.js/blob/d76f0b13/packages/next/src/build/index.ts#L573
2222+ const nextRemotePatterns = nextConfig.images?.remotePatterns ?? [];
2323+2424+ const remotePatterns = nextRemotePatterns.map((p) => ({
2525+ protocol: p.protocol,
2626+ hostname: p.hostname ? pm.makeRe(p.hostname).source : undefined,
2727+ port: p.port,
2828+ pathname: pm.makeRe(p.pathname ?? "**", { dot: true }).source,
2929+ // search is canary only as of June 2025
3030+ search: (p as any).search,
3131+ }));
3232+3333+ // Local patterns are only in canary as of June 2025
3434+ const nextLocalPatterns = (nextConfig.images as any)?.localPatterns ?? [];
3535+3636+ // https://github.com/vercel/next.js/blob/d76f0b13/packages/next/src/build/index.ts#L573
3737+ const localPatterns = nextLocalPatterns.map((p: any) => ({
3838+ pathname: pm.makeRe(p.pathname ?? "**", { dot: true }).source,
3939+ search: p.search,
4040+ }));
4141+1942 await build({
2043 entryPoints: [initPath],
2144 outdir: path.join(options.outputDir, "cloudflare"),
···2750 define: {
2851 __BUILD_TIMESTAMP_MS__: JSON.stringify(Date.now()),
2952 __NEXT_BASE_PATH__: JSON.stringify(basePath),
5353+ __IMAGES_REMOTE_PATTERNS__: JSON.stringify(remotePatterns),
5454+ __IMAGES_LOCAL_PATTERNS__: JSON.stringify(localPatterns),
3055 },
3156 });
3257}
+98
packages/cloudflare/src/cli/templates/init.ts
···140140 process.env.__NEXT_PRIVATE_ORIGIN = url.origin;
141141}
142142143143+export type RemotePattern = {
144144+ protocol?: "http" | "https";
145145+ hostname: string;
146146+ port?: string;
147147+ pathname: string;
148148+ search?: string;
149149+};
150150+151151+const imgRemotePatterns = __IMAGES_REMOTE_PATTERNS__;
152152+153153+/**
154154+ * Fetches an images.
155155+ *
156156+ * Local images (starting with a '/' as fetched using the passed fetcher).
157157+ * Remote images should match the configured remote patterns or a 404 response is returned.
158158+ */
159159+export function fetchImage(fetcher: Fetcher | undefined, url: string) {
160160+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
161161+ if (!url || url.length > 3072 || url.startsWith("//")) {
162162+ return new Response("Not Found", { status: 404 });
163163+ }
164164+165165+ // Local
166166+ if (url.startsWith("/")) {
167167+ if (/\/_next\/image($|\/)/.test(decodeURIComponent(parseUrl(url)?.pathname ?? ""))) {
168168+ return new Response("Not Found", { status: 404 });
169169+ }
170170+171171+ return fetcher?.fetch(`http://assets.local${url}`);
172172+ }
173173+174174+ // Remote
175175+ let hrefParsed: URL;
176176+ try {
177177+ hrefParsed = new URL(url);
178178+ } catch {
179179+ return new Response("Not Found", { status: 404 });
180180+ }
181181+182182+ if (!["http:", "https:"].includes(hrefParsed.protocol)) {
183183+ return new Response("Not Found", { status: 404 });
184184+ }
185185+186186+ if (!imgRemotePatterns.some((p: RemotePattern) => matchRemotePattern(p, hrefParsed))) {
187187+ return new Response("Not Found", { status: 404 });
188188+ }
189189+190190+ return fetch(url, { cf: { cacheEverything: true } });
191191+}
192192+193193+export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
194194+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
195195+ if (pattern.protocol !== undefined) {
196196+ if (pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) {
197197+ return false;
198198+ }
199199+ }
200200+ if (pattern.port !== undefined) {
201201+ if (pattern.port !== url.port) {
202202+ return false;
203203+ }
204204+ }
205205+206206+ if (pattern.hostname === undefined) {
207207+ throw new Error(`Pattern should define hostname but found\n${JSON.stringify(pattern)}`);
208208+ } else {
209209+ if (!new RegExp(pattern.hostname).test(url.hostname)) {
210210+ return false;
211211+ }
212212+ }
213213+214214+ if (pattern.search !== undefined) {
215215+ if (pattern.search !== url.search) {
216216+ return false;
217217+ }
218218+ }
219219+220220+ // Should be the same as writeImagesManifest()
221221+ if (!new RegExp(pattern.pathname).test(url.pathname)) {
222222+ return false;
223223+ }
224224+225225+ return true;
226226+}
227227+228228+function parseUrl(url: string): URL | undefined {
229229+ let parsed: URL | undefined = undefined;
230230+ try {
231231+ parsed = new URL(url, "http://n");
232232+ } catch {
233233+ // empty
234234+ }
235235+ return parsed;
236236+}
237237+143238/* eslint-disable no-var */
144239declare global {
145240 // Build timestamp
146241 var __BUILD_TIMESTAMP_MS__: number;
147242 // Next basePath
148243 var __NEXT_BASE_PATH__: string;
244244+ // Images patterns
245245+ var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
246246+ var __IMAGES_LOCAL_PATTERNS__: unknown[];
149247}
150248/* eslint-enable no-var */
+2-4
packages/cloudflare/src/cli/templates/worker.ts
···11//@ts-expect-error: Will be resolved by wrangler build
22-import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
22+import { fetchImage, runWithCloudflareRequestContext } from "./cloudflare/init.js";
33// @ts-expect-error: Will be resolved by wrangler build
44import { handler as middlewareHandler } from "./middleware/handler.mjs";
55···3131 // Fallback for the Next default image loader.
3232 if (url.pathname === `${globalThis.__NEXT_BASE_PATH__}/_next/image`) {
3333 const imageUrl = url.searchParams.get("url") ?? "";
3434- return imageUrl.startsWith("/")
3535- ? env.ASSETS?.fetch(`http://assets.local${imageUrl}`)
3636- : fetch(imageUrl, { cf: { cacheEverything: true } });
3434+ return fetchImage(env.ASSETS, imageUrl);
3735 }
38363937 // - `Request`s are handled by the Next server