···11+/**
22+ * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-remote-pattern.test.ts
33+ */
44+55+import pm from "picomatch";
66+import { describe, expect, it } from "vitest";
77+88+import { matchRemotePattern as m } from "./images.js";
99+1010+describe("matchRemotePattern", () => {
1111+ it("should match literal hostname", () => {
1212+ const p = { hostname: pm.makeRe("example.com") } as const;
1313+ expect(m(p, new URL("https://example.com"))).toBe(true);
1414+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
1515+ expect(m(p, new URL("https://example.net"))).toBe(false);
1616+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
1717+ expect(m(p, new URL("https://com"))).toBe(false);
1818+ expect(m(p, new URL("https://example.com/path"))).toBe(true);
1919+ expect(m(p, new URL("https://example.com/path/to"))).toBe(true);
2020+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true);
2121+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true);
2222+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true);
2323+ expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(true);
2424+ });
2525+2626+ it("should match literal protocol and hostname", () => {
2727+ const p = { protocol: "https", hostname: pm.makeRe("example.com") } as const;
2828+ expect(m(p, new URL("https://example.com"))).toBe(true);
2929+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
3030+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
3131+ expect(m(p, new URL("https://com"))).toBe(false);
3232+ expect(m(p, new URL("https://example.com/path/to"))).toBe(true);
3333+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true);
3434+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true);
3535+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true);
3636+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true);
3737+ expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false);
3838+ expect(m(p, new URL("ftp://example.com:81/path/to/file"))).toBe(false);
3939+ });
4040+4141+ it("should match literal protocol, hostname, no port", () => {
4242+ const p = { protocol: "https", hostname: pm.makeRe("example.com"), port: "" } as const;
4343+ expect(m(p, new URL("https://example.com"))).toBe(true);
4444+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
4545+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
4646+ expect(m(p, new URL("https://com"))).toBe(false);
4747+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true);
4848+ expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(true);
4949+ expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false);
5050+ expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false);
5151+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false);
5252+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false);
5353+ expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false);
5454+ });
5555+5656+ it("should match literal protocol, hostname, no port, no search", () => {
5757+ const p = {
5858+ protocol: "https",
5959+ hostname: pm.makeRe("example.com"),
6060+ port: "",
6161+ search: "",
6262+ } as const;
6363+ expect(m(p, new URL("https://example.com"))).toBe(true);
6464+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
6565+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
6666+ expect(m(p, new URL("https://com"))).toBe(false);
6767+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true);
6868+ expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false);
6969+ expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false);
7070+ expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false);
7171+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false);
7272+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false);
7373+ expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false);
7474+ });
7575+7676+ it("should match literal protocol, hostname, port 42", () => {
7777+ const p = {
7878+ protocol: "https",
7979+ hostname: pm.makeRe("example.com"),
8080+ port: "42",
8181+ } as const;
8282+ expect(m(p, new URL("https://example.com:42"))).toBe(true);
8383+ expect(m(p, new URL("https://example.com.uk:42"))).toBe(false);
8484+ expect(m(p, new URL("https://sub.example.com:42"))).toBe(false);
8585+ expect(m(p, new URL("https://com:42"))).toBe(false);
8686+ expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true);
8787+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true);
8888+ expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false);
8989+ expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false);
9090+ expect(m(p, new URL("https://example.com"))).toBe(false);
9191+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
9292+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
9393+ expect(m(p, new URL("https://com"))).toBe(false);
9494+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false);
9595+ expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false);
9696+ expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false);
9797+ expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false);
9898+ expect(m(p, new URL("https://example.com:81"))).toBe(false);
9999+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false);
100100+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false);
101101+ });
102102+103103+ it("should match literal protocol, hostname, port, pathname", () => {
104104+ const p = {
105105+ protocol: "https",
106106+ hostname: pm.makeRe("example.com"),
107107+ port: "42",
108108+ pathname: pm.makeRe("/path/to/file", { dot: true }),
109109+ } as const;
110110+ expect(m(p, new URL("https://example.com:42"))).toBe(false);
111111+ expect(m(p, new URL("https://example.com.uk:42"))).toBe(false);
112112+ expect(m(p, new URL("https://sub.example.com:42"))).toBe(false);
113113+ expect(m(p, new URL("https://example.com:42/path"))).toBe(false);
114114+ expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false);
115115+ expect(m(p, new URL("https://example.com:42/file"))).toBe(false);
116116+ expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true);
117117+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true);
118118+ expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false);
119119+ expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false);
120120+ expect(m(p, new URL("https://example.com"))).toBe(false);
121121+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
122122+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
123123+ expect(m(p, new URL("https://example.com/path"))).toBe(false);
124124+ expect(m(p, new URL("https://example.com/path/to"))).toBe(false);
125125+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false);
126126+ expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false);
127127+ expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false);
128128+ expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false);
129129+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false);
130130+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false);
131131+ });
132132+133133+ it("should match literal protocol, hostname, port, pathname, search", () => {
134134+ const p = {
135135+ protocol: "https",
136136+ hostname: pm.makeRe("example.com"),
137137+ port: "42",
138138+ pathname: pm.makeRe("/path/to/file", { dot: true }),
139139+ search: "?q=1&a=two&s=!@$^&-_+/()[]{};:~",
140140+ } as const;
141141+ expect(m(p, new URL("https://example.com:42"))).toBe(false);
142142+ expect(m(p, new URL("https://example.com.uk:42"))).toBe(false);
143143+ expect(m(p, new URL("https://sub.example.com:42"))).toBe(false);
144144+ expect(m(p, new URL("https://example.com:42/path"))).toBe(false);
145145+ expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false);
146146+ expect(m(p, new URL("https://example.com:42/file"))).toBe(false);
147147+ expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(false);
148148+ expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false);
149149+ expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false);
150150+ expect(m(p, new URL("https://example.com"))).toBe(false);
151151+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
152152+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
153153+ expect(m(p, new URL("https://example.com/path"))).toBe(false);
154154+ expect(m(p, new URL("https://example.com/path/to"))).toBe(false);
155155+ expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false);
156156+ expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false);
157157+ expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false);
158158+ expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false);
159159+ expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false);
160160+ expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false);
161161+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(false);
162162+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two"))).toBe(false);
163163+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s"))).toBe(false);
164164+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s="))).toBe(false);
165165+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@"))).toBe(false);
166166+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@$^&-_+/()[]{};:~"))).toBe(true);
167167+ expect(m(p, new URL("https://example.com:42/path/to/file?q=1&s=!@$^&-_+/()[]{};:~&a=two"))).toBe(false);
168168+ expect(m(p, new URL("https://example.com:42/path/to/file?a=two&q=1&s=!@$^&-_+/()[]{};:~"))).toBe(false);
169169+ });
170170+171171+ it("should match hostname pattern with single asterisk by itself", () => {
172172+ const p = { hostname: pm.makeRe("avatars.*.example.com") } as const;
173173+ expect(m(p, new URL("https://com"))).toBe(false);
174174+ expect(m(p, new URL("https://example.com"))).toBe(false);
175175+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
176176+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
177177+ expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false);
178178+ expect(m(p, new URL("https://avatars.example.com"))).toBe(false);
179179+ expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true);
180180+ expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true);
181181+ expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false);
182182+ });
183183+184184+ it("should match hostname pattern with single asterisk at beginning", () => {
185185+ const p = { hostname: pm.makeRe("avatars.*1.example.com") } as const;
186186+ expect(m(p, new URL("https://com"))).toBe(false);
187187+ expect(m(p, new URL("https://example.com"))).toBe(false);
188188+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
189189+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
190190+ expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false);
191191+ expect(m(p, new URL("https://avatars.example.com"))).toBe(false);
192192+ expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true);
193193+ expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true);
194194+ expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false);
195195+ expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false);
196196+ expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(false);
197197+ expect(m(p, new URL("https://avatars.1.example.com"))).toBe(true);
198198+ });
199199+200200+ it("should match hostname pattern with single asterisk in middle", () => {
201201+ const p = { hostname: pm.makeRe("avatars.*a*.example.com") } as const;
202202+ expect(m(p, new URL("https://com"))).toBe(false);
203203+ expect(m(p, new URL("https://example.com"))).toBe(false);
204204+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
205205+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
206206+ expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false);
207207+ expect(m(p, new URL("https://avatars.example.com"))).toBe(false);
208208+ expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false);
209209+ expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true);
210210+ expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false);
211211+ expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false);
212212+ expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true);
213213+ expect(m(p, new URL("https://avatars.a.example.com"))).toBe(true);
214214+ });
215215+216216+ it("should match hostname pattern with single asterisk at end", () => {
217217+ const p = { hostname: pm.makeRe("avatars.ia*.example.com") } as const;
218218+ expect(m(p, new URL("https://com"))).toBe(false);
219219+ expect(m(p, new URL("https://example.com"))).toBe(false);
220220+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
221221+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
222222+ expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false);
223223+ expect(m(p, new URL("https://avatars.example.com"))).toBe(false);
224224+ expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false);
225225+ expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true);
226226+ expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false);
227227+ expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false);
228228+ expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true);
229229+ expect(m(p, new URL("https://avatars.ia.example.com"))).toBe(true);
230230+ });
231231+232232+ it("should match hostname pattern with double asterisk", () => {
233233+ const p = { hostname: pm.makeRe("**.example.com") } as const;
234234+ expect(m(p, new URL("https://com"))).toBe(false);
235235+ expect(m(p, new URL("https://example.com"))).toBe(false);
236236+ expect(m(p, new URL("https://sub.example.com"))).toBe(true);
237237+ expect(m(p, new URL("https://deep.sub.example.com"))).toBe(true);
238238+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
239239+ expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false);
240240+ expect(m(p, new URL("https://avatars.example.com"))).toBe(true);
241241+ expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true);
242242+ expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true);
243243+ expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(true);
244244+ });
245245+246246+ it("should match pathname pattern with single asterisk by itself", () => {
247247+ const p = {
248248+ hostname: pm.makeRe("example.com"),
249249+ pathname: pm.makeRe("/act123/*/pic.jpg", { dot: true }),
250250+ } as const;
251251+ expect(m(p, new URL("https://com"))).toBe(false);
252252+ expect(m(p, new URL("https://example.com"))).toBe(false);
253253+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
254254+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
255255+ expect(m(p, new URL("https://example.com/act123"))).toBe(false);
256256+ expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false);
257257+ expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false);
258258+ expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false);
259259+ expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true);
260260+ expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true);
261261+ expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true);
262262+ expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true);
263263+ expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false);
264264+ expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true);
265265+ expect(m(p, new URL("https://example.com/act123/team/usr4/pic.jpg"))).toBe(false);
266266+ expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false);
267267+ });
268268+269269+ it("should match pathname pattern with single asterisk at the beginning", () => {
270270+ const p = {
271271+ hostname: pm.makeRe("example.com"),
272272+ pathname: pm.makeRe("/act123/*4/pic.jpg", { dot: true }),
273273+ } as const;
274274+ expect(m(p, new URL("https://com"))).toBe(false);
275275+ expect(m(p, new URL("https://example.com"))).toBe(false);
276276+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
277277+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
278278+ expect(m(p, new URL("https://example.com/act123"))).toBe(false);
279279+ expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false);
280280+ expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false);
281281+ expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false);
282282+ expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true);
283283+ expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(false);
284284+ expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(true);
285285+ expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false);
286286+ expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false);
287287+ expect(m(p, new URL("https://example.com/act123/4/pic.jpg"))).toBe(true);
288288+ });
289289+290290+ it("should match pathname pattern with single asterisk in the middle", () => {
291291+ const p = {
292292+ hostname: pm.makeRe("example.com"),
293293+ pathname: pm.makeRe("/act123/*sr*/pic.jpg", { dot: true }),
294294+ } as const;
295295+ expect(m(p, new URL("https://com"))).toBe(false);
296296+ expect(m(p, new URL("https://example.com"))).toBe(false);
297297+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
298298+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
299299+ expect(m(p, new URL("https://example.com/act123"))).toBe(false);
300300+ expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false);
301301+ expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false);
302302+ expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false);
303303+ expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true);
304304+ expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true);
305305+ expect(m(p, new URL("https://example.com/act123/.sr6/pic.jpg"))).toBe(true);
306306+ expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false);
307307+ expect(m(p, new URL("https://example.com/act123/team5/pic.jpg"))).toBe(false);
308308+ expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false);
309309+ expect(m(p, new URL("https://example.com/act123/sr/pic.jpg"))).toBe(true);
310310+ });
311311+312312+ it("should match pathname pattern with single asterisk at the end", () => {
313313+ const p = {
314314+ hostname: pm.makeRe("example.com"),
315315+ pathname: pm.makeRe("/act123/usr*/pic.jpg", { dot: true }),
316316+ } as const;
317317+ expect(m(p, new URL("https://com"))).toBe(false);
318318+ expect(m(p, new URL("https://example.com"))).toBe(false);
319319+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
320320+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
321321+ expect(m(p, new URL("https://example.com/act123"))).toBe(false);
322322+ expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false);
323323+ expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false);
324324+ expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false);
325325+ expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true);
326326+ expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true);
327327+ expect(m(p, new URL("https://example.com/act123/usr/pic.jpg"))).toBe(true);
328328+ expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false);
329329+ expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false);
330330+ expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false);
331331+ expect(m(p, new URL("https://sub.example.com/act123/usr6/pic.jpg"))).toBe(false);
332332+ });
333333+334334+ it("should match pathname pattern with double asterisk", () => {
335335+ const p = {
336336+ hostname: pm.makeRe("example.com"),
337337+ pathname: pm.makeRe("/act123/**", { dot: true }),
338338+ } as const;
339339+ expect(m(p, new URL("https://com"))).toBe(false);
340340+ expect(m(p, new URL("https://example.com"))).toBe(false);
341341+ expect(m(p, new URL("https://sub.example.com"))).toBe(false);
342342+ expect(m(p, new URL("https://example.com.uk"))).toBe(false);
343343+ expect(m(p, new URL("https://example.com/act123"))).toBe(true);
344344+ expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(true);
345345+ expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(true);
346346+ expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(true);
347347+ expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true);
348348+ expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true);
349349+ expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true);
350350+ expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true);
351351+ expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true);
352352+ expect(m(p, new URL("https://example.com/act123/team/.pic.jpg"))).toBe(true);
353353+ expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false);
354354+ expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false);
355355+ expect(m(p, new URL("https://sub.example.com/act123/team/pic.jpg"))).toBe(false);
356356+ });
357357+358358+ it("should throw when hostname is missing", () => {
359359+ const p = { protocol: "https" } as const;
360360+ // @ts-ignore testing invalid input
361361+ expect(m(p, new URL("https://example.com"))).toBe(false);
362362+ });
363363+});
+98
packages/cloudflare/src/cli/templates/images.ts
···11+export type RemotePattern = {
22+ protocol?: "http" | "https";
33+ hostname: string;
44+ port?: string;
55+ // pathname is always set in the manifest (to `makeRe(pathname ?? '**', { dot: true }).source`)
66+ pathname: string;
77+ search?: string;
88+};
99+1010+/**
1111+ * Fetches an images.
1212+ *
1313+ * Local images (starting with a '/' as fetched using the passed fetcher).
1414+ * Remote images should match the configured remote patterns or a 404 response is returned.
1515+ */
1616+export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) {
1717+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208
1818+ if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) {
1919+ return getUrlErrorResponse();
2020+ }
2121+2222+ // Local
2323+ if (imageUrl.startsWith("/")) {
2424+ let pathname: string;
2525+ try {
2626+ const url = new URL(imageUrl, "http://n");
2727+ pathname = decodeURIComponent(url.pathname);
2828+ } catch {
2929+ return getUrlErrorResponse();
3030+ }
3131+ if (/\/_next\/image($|\/)/.test(pathname)) {
3232+ return getUrlErrorResponse();
3333+ }
3434+3535+ return fetcher?.fetch(`http://assets.local${imageUrl}`);
3636+ }
3737+3838+ // Remote
3939+ let url: URL;
4040+ try {
4141+ url = new URL(imageUrl);
4242+ } catch {
4343+ return getUrlErrorResponse();
4444+ }
4545+4646+ if (url.protocol !== "http:" && url.protocol !== "https:") {
4747+ return getUrlErrorResponse();
4848+ }
4949+5050+ if (!__IMAGES_REMOTE_PATTERNS__.some((p: RemotePattern) => matchRemotePattern(p, url))) {
5151+ return getUrlErrorResponse();
5252+ }
5353+5454+ return fetch(imageUrl, { cf: { cacheEverything: true } });
5555+}
5656+5757+export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean {
5858+ // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
5959+ if (
6060+ pattern.protocol !== undefined &&
6161+ pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")
6262+ ) {
6363+ return false;
6464+ }
6565+6666+ if (pattern.port !== undefined && pattern.port !== url.port) {
6767+ return false;
6868+ }
6969+7070+ if (pattern.hostname === undefined || !new RegExp(pattern.hostname).test(url.hostname)) {
7171+ return false;
7272+ }
7373+7474+ if (pattern.search !== undefined && pattern.search !== url.search) {
7575+ return false;
7676+ }
7777+7878+ // Should be the same as writeImagesManifest()
7979+ if (!new RegExp(pattern.pathname).test(url.pathname)) {
8080+ return false;
8181+ }
8282+8383+ return true;
8484+}
8585+8686+/**
8787+ * @returns same error as Next.js when the url query parameter is not accepted.
8888+ */
8989+function getUrlErrorResponse() {
9090+ return new Response(`"url" parameter is not allowed`, { status: 400 });
9191+}
9292+9393+/* eslint-disable no-var */
9494+declare global {
9595+ var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
9696+ var __IMAGES_LOCAL_PATTERNS__: unknown[];
9797+}
9898+/* eslint-enable no-var */
-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-238143/* eslint-disable no-var */
239144declare global {
240145 // Build timestamp
241146 var __BUILD_TIMESTAMP_MS__: number;
242147 // Next basePath
243148 var __NEXT_BASE_PATH__: string;
244244- // Images patterns
245245- var __IMAGES_REMOTE_PATTERNS__: RemotePattern[];
246246- var __IMAGES_LOCAL_PATTERNS__: unknown[];
247149}
248150/* eslint-enable no-var */
+3-1
packages/cloudflare/src/cli/templates/worker.ts
···11//@ts-expect-error: Will be resolved by wrangler build
22-import { fetchImage, runWithCloudflareRequestContext } from "./cloudflare/init.js";
22+import { fetchImage } from "./cloudflare/images.js";
33+//@ts-expect-error: Will be resolved by wrangler build
44+import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
35// @ts-expect-error: Will be resolved by wrangler build
46import { handler as middlewareHandler } from "./middleware/handler.mjs";
57