this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add tests for remotePatterns (#730)

authored by

Victor Berchet and committed by
GitHub
ae317335 689327de

+530 -127
+5
.changeset/bright-readers-love.md
··· 1 + --- 2 + "@opennextjs/cloudflare": patch 3 + --- 4 + 5 + add tests for remote patterns
+11
examples/playground14/e2e/cloudflare.spec.ts
··· 18 18 const res = await page.request.get("/api/env"); 19 19 await expect(res.json()).resolves.toEqual(expect.objectContaining({ PROCESS_ENV_VAR: "process.env" })); 20 20 }); 21 + 22 + test("fetch an image allowed by remotePatterns", async ({ page }) => { 23 + const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248818"); 24 + expect(res.status()).toBe(200); 25 + expect(res.headers()).toMatchObject({ "content-type": "image/jpeg" }); 26 + }); 27 + 28 + test("404 when fetching an image disallowed by remotePatterns", async ({ page }) => { 29 + const res = await page.request.get("/_next/image?url=https://avatars.githubusercontent.com/u/248817"); 30 + expect(res.status()).toBe(400); 31 + }); 21 32 });
+9
examples/playground14/next.config.mjs
··· 11 11 serverSourceMaps: true, 12 12 instrumentationHook: true, 13 13 }, 14 + images: { 15 + remotePatterns: [ 16 + { 17 + protocol: "https", 18 + hostname: "avatars.githubusercontent.com", 19 + pathname: "/u/248818", 20 + }, 21 + ], 22 + }, 14 23 }; 15 24 16 25 export default nextConfig;
+5 -2
packages/cloudflare/src/cli/build/build.ts
··· 3 3 import { createCacheAssets, createStaticAssets } from "@opennextjs/aws/build/createAssets.js"; 4 4 import { createMiddleware } from "@opennextjs/aws/build/createMiddleware.js"; 5 5 import * as buildHelper from "@opennextjs/aws/build/helper.js"; 6 - import { BuildOptions } from "@opennextjs/aws/build/helper.js"; 7 6 import { printHeader } from "@opennextjs/aws/build/utils.js"; 8 7 import logger from "@opennextjs/aws/logger.js"; 9 8 ··· 12 11 import { bundleServer } from "./bundle-server.js"; 13 12 import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js"; 14 13 import { compileEnvFiles } from "./open-next/compile-env-files.js"; 14 + import { compileImages } from "./open-next/compile-images.js"; 15 15 import { compileInit } from "./open-next/compile-init.js"; 16 16 import { compileDurableObjects } from "./open-next/compileDurableObjects.js"; 17 17 import { createServerBundle } from "./open-next/createServerBundle.js"; ··· 28 28 * @param projectOpts The options for the project 29 29 */ 30 30 export async function build( 31 - options: BuildOptions, 31 + options: buildHelper.BuildOptions, 32 32 config: OpenNextConfig, 33 33 projectOpts: ProjectOptions 34 34 ): Promise<void> { ··· 66 66 67 67 // Compile workerd init 68 68 compileInit(options); 69 + 70 + // Compile image helpers 71 + compileImages(options); 69 72 70 73 // Compile middleware 71 74 await createMiddleware(options, { forceOnlyBuildOnce: true });
+34
packages/cloudflare/src/cli/build/open-next/compile-images.ts
··· 1 + import fs from "node:fs"; 2 + import path from "node:path"; 3 + import { fileURLToPath } from "node:url"; 4 + 5 + import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; 6 + import { build } from "esbuild"; 7 + 8 + /** 9 + * Compiles the initialization code for the workerd runtime 10 + */ 11 + export async function compileImages(options: BuildOptions) { 12 + const currentDir = path.join(path.dirname(fileURLToPath(import.meta.url))); 13 + const templatesDir = path.join(currentDir, "../../templates"); 14 + const imagesPath = path.join(templatesDir, "images.js"); 15 + 16 + const imagesManifestPath = path.join(options.appBuildOutputPath, ".next/images-manifest.json"); 17 + const imagesManifest = fs.existsSync(imagesManifestPath) 18 + ? JSON.parse(fs.readFileSync(imagesManifestPath, { encoding: "utf-8" })) 19 + : {}; 20 + 21 + await build({ 22 + entryPoints: [imagesPath], 23 + outdir: path.join(options.outputDir, "cloudflare"), 24 + bundle: false, 25 + minify: false, 26 + format: "esm", 27 + target: "esnext", 28 + platform: "node", 29 + define: { 30 + __IMAGES_REMOTE_PATTERNS__: JSON.stringify(imagesManifest?.images?.remotePatterns ?? []), 31 + __IMAGES_LOCAL_PATTERNS__: JSON.stringify(imagesManifest?.images?.localPatterns ?? []), 32 + }, 33 + }); 34 + }
-25
packages/cloudflare/src/cli/build/open-next/compile-init.ts
··· 1 - /* eslint-disable @typescript-eslint/no-explicit-any */ 2 1 import path from "node:path"; 3 2 import { fileURLToPath } from "node:url"; 4 3 5 4 import { loadConfig } from "@opennextjs/aws/adapters/config/util.js"; 6 5 import type { BuildOptions } from "@opennextjs/aws/build/helper.js"; 7 6 import { build } from "esbuild"; 8 - import pm from "picomatch"; 9 7 10 8 /** 11 9 * Compiles the initialization code for the workerd runtime ··· 18 16 const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next")); 19 17 const basePath = nextConfig.basePath ?? ""; 20 18 21 - // https://github.com/vercel/next.js/blob/d76f0b13/packages/next/src/build/index.ts#L573 22 - const nextRemotePatterns = nextConfig.images?.remotePatterns ?? []; 23 - 24 - const remotePatterns = nextRemotePatterns.map((p) => ({ 25 - protocol: p.protocol, 26 - hostname: p.hostname ? pm.makeRe(p.hostname).source : undefined, 27 - port: p.port, 28 - pathname: pm.makeRe(p.pathname ?? "**", { dot: true }).source, 29 - // search is canary only as of June 2025 30 - search: (p as any).search, 31 - })); 32 - 33 - // Local patterns are only in canary as of June 2025 34 - const nextLocalPatterns = (nextConfig.images as any)?.localPatterns ?? []; 35 - 36 - // https://github.com/vercel/next.js/blob/d76f0b13/packages/next/src/build/index.ts#L573 37 - const localPatterns = nextLocalPatterns.map((p: any) => ({ 38 - pathname: pm.makeRe(p.pathname ?? "**", { dot: true }).source, 39 - search: p.search, 40 - })); 41 - 42 19 await build({ 43 20 entryPoints: [initPath], 44 21 outdir: path.join(options.outputDir, "cloudflare"), ··· 50 27 define: { 51 28 __BUILD_TIMESTAMP_MS__: JSON.stringify(Date.now()), 52 29 __NEXT_BASE_PATH__: JSON.stringify(basePath), 53 - __IMAGES_REMOTE_PATTERNS__: JSON.stringify(remotePatterns), 54 - __IMAGES_LOCAL_PATTERNS__: JSON.stringify(localPatterns), 55 30 }, 56 31 }); 57 32 }
+363
packages/cloudflare/src/cli/templates/images.spec.ts
··· 1 + /** 2 + * See https://github.com/vercel/next.js/blob/64702a9/test/unit/image-optimizer/match-remote-pattern.test.ts 3 + */ 4 + 5 + import pm from "picomatch"; 6 + import { describe, expect, it } from "vitest"; 7 + 8 + import { matchRemotePattern as m } from "./images.js"; 9 + 10 + describe("matchRemotePattern", () => { 11 + it("should match literal hostname", () => { 12 + const p = { hostname: pm.makeRe("example.com") } as const; 13 + expect(m(p, new URL("https://example.com"))).toBe(true); 14 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 15 + expect(m(p, new URL("https://example.net"))).toBe(false); 16 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 17 + expect(m(p, new URL("https://com"))).toBe(false); 18 + expect(m(p, new URL("https://example.com/path"))).toBe(true); 19 + expect(m(p, new URL("https://example.com/path/to"))).toBe(true); 20 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); 21 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true); 22 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); 23 + expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(true); 24 + }); 25 + 26 + it("should match literal protocol and hostname", () => { 27 + const p = { protocol: "https", hostname: pm.makeRe("example.com") } as const; 28 + expect(m(p, new URL("https://example.com"))).toBe(true); 29 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 30 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 31 + expect(m(p, new URL("https://com"))).toBe(false); 32 + expect(m(p, new URL("https://example.com/path/to"))).toBe(true); 33 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); 34 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); 35 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(true); 36 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(true); 37 + expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); 38 + expect(m(p, new URL("ftp://example.com:81/path/to/file"))).toBe(false); 39 + }); 40 + 41 + it("should match literal protocol, hostname, no port", () => { 42 + const p = { protocol: "https", hostname: pm.makeRe("example.com"), port: "" } as const; 43 + expect(m(p, new URL("https://example.com"))).toBe(true); 44 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 45 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 46 + expect(m(p, new URL("https://com"))).toBe(false); 47 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); 48 + expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(true); 49 + expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); 50 + expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); 51 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); 52 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); 53 + expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); 54 + }); 55 + 56 + it("should match literal protocol, hostname, no port, no search", () => { 57 + const p = { 58 + protocol: "https", 59 + hostname: pm.makeRe("example.com"), 60 + port: "", 61 + search: "", 62 + } as const; 63 + expect(m(p, new URL("https://example.com"))).toBe(true); 64 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 65 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 66 + expect(m(p, new URL("https://com"))).toBe(false); 67 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(true); 68 + expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); 69 + expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); 70 + expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); 71 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); 72 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); 73 + expect(m(p, new URL("http://example.com:81/path/to/file"))).toBe(false); 74 + }); 75 + 76 + it("should match literal protocol, hostname, port 42", () => { 77 + const p = { 78 + protocol: "https", 79 + hostname: pm.makeRe("example.com"), 80 + port: "42", 81 + } as const; 82 + expect(m(p, new URL("https://example.com:42"))).toBe(true); 83 + expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); 84 + expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); 85 + expect(m(p, new URL("https://com:42"))).toBe(false); 86 + expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true); 87 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); 88 + expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); 89 + expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); 90 + expect(m(p, new URL("https://example.com"))).toBe(false); 91 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 92 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 93 + expect(m(p, new URL("https://com"))).toBe(false); 94 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); 95 + expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); 96 + expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); 97 + expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); 98 + expect(m(p, new URL("https://example.com:81"))).toBe(false); 99 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); 100 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); 101 + }); 102 + 103 + it("should match literal protocol, hostname, port, pathname", () => { 104 + const p = { 105 + protocol: "https", 106 + hostname: pm.makeRe("example.com"), 107 + port: "42", 108 + pathname: pm.makeRe("/path/to/file", { dot: true }), 109 + } as const; 110 + expect(m(p, new URL("https://example.com:42"))).toBe(false); 111 + expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); 112 + expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); 113 + expect(m(p, new URL("https://example.com:42/path"))).toBe(false); 114 + expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false); 115 + expect(m(p, new URL("https://example.com:42/file"))).toBe(false); 116 + expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(true); 117 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(true); 118 + expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); 119 + expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); 120 + expect(m(p, new URL("https://example.com"))).toBe(false); 121 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 122 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 123 + expect(m(p, new URL("https://example.com/path"))).toBe(false); 124 + expect(m(p, new URL("https://example.com/path/to"))).toBe(false); 125 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); 126 + expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); 127 + expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); 128 + expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); 129 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); 130 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); 131 + }); 132 + 133 + it("should match literal protocol, hostname, port, pathname, search", () => { 134 + const p = { 135 + protocol: "https", 136 + hostname: pm.makeRe("example.com"), 137 + port: "42", 138 + pathname: pm.makeRe("/path/to/file", { dot: true }), 139 + search: "?q=1&a=two&s=!@$^&-_+/()[]{};:~", 140 + } as const; 141 + expect(m(p, new URL("https://example.com:42"))).toBe(false); 142 + expect(m(p, new URL("https://example.com.uk:42"))).toBe(false); 143 + expect(m(p, new URL("https://sub.example.com:42"))).toBe(false); 144 + expect(m(p, new URL("https://example.com:42/path"))).toBe(false); 145 + expect(m(p, new URL("https://example.com:42/path/to"))).toBe(false); 146 + expect(m(p, new URL("https://example.com:42/file"))).toBe(false); 147 + expect(m(p, new URL("https://example.com:42/path/to/file"))).toBe(false); 148 + expect(m(p, new URL("http://example.com:42/path/to/file"))).toBe(false); 149 + expect(m(p, new URL("ftp://example.com:42/path/to/file"))).toBe(false); 150 + expect(m(p, new URL("https://example.com"))).toBe(false); 151 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 152 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 153 + expect(m(p, new URL("https://example.com/path"))).toBe(false); 154 + expect(m(p, new URL("https://example.com/path/to"))).toBe(false); 155 + expect(m(p, new URL("https://example.com/path/to/file"))).toBe(false); 156 + expect(m(p, new URL("https://example.com/path/to/file?q=1"))).toBe(false); 157 + expect(m(p, new URL("http://example.com/path/to/file"))).toBe(false); 158 + expect(m(p, new URL("ftp://example.com/path/to/file"))).toBe(false); 159 + expect(m(p, new URL("https://example.com:81/path/to/file"))).toBe(false); 160 + expect(m(p, new URL("https://example.com:81/path/to/file?q=1"))).toBe(false); 161 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1"))).toBe(false); 162 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two"))).toBe(false); 163 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s"))).toBe(false); 164 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s="))).toBe(false); 165 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@"))).toBe(false); 166 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&a=two&s=!@$^&-_+/()[]{};:~"))).toBe(true); 167 + expect(m(p, new URL("https://example.com:42/path/to/file?q=1&s=!@$^&-_+/()[]{};:~&a=two"))).toBe(false); 168 + expect(m(p, new URL("https://example.com:42/path/to/file?a=two&q=1&s=!@$^&-_+/()[]{};:~"))).toBe(false); 169 + }); 170 + 171 + it("should match hostname pattern with single asterisk by itself", () => { 172 + const p = { hostname: pm.makeRe("avatars.*.example.com") } as const; 173 + expect(m(p, new URL("https://com"))).toBe(false); 174 + expect(m(p, new URL("https://example.com"))).toBe(false); 175 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 176 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 177 + expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); 178 + expect(m(p, new URL("https://avatars.example.com"))).toBe(false); 179 + expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); 180 + expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); 181 + expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); 182 + }); 183 + 184 + it("should match hostname pattern with single asterisk at beginning", () => { 185 + const p = { hostname: pm.makeRe("avatars.*1.example.com") } as const; 186 + expect(m(p, new URL("https://com"))).toBe(false); 187 + expect(m(p, new URL("https://example.com"))).toBe(false); 188 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 189 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 190 + expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); 191 + expect(m(p, new URL("https://avatars.example.com"))).toBe(false); 192 + expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); 193 + expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); 194 + expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); 195 + expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); 196 + expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(false); 197 + expect(m(p, new URL("https://avatars.1.example.com"))).toBe(true); 198 + }); 199 + 200 + it("should match hostname pattern with single asterisk in middle", () => { 201 + const p = { hostname: pm.makeRe("avatars.*a*.example.com") } as const; 202 + expect(m(p, new URL("https://com"))).toBe(false); 203 + expect(m(p, new URL("https://example.com"))).toBe(false); 204 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 205 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 206 + expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); 207 + expect(m(p, new URL("https://avatars.example.com"))).toBe(false); 208 + expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); 209 + expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); 210 + expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); 211 + expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); 212 + expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true); 213 + expect(m(p, new URL("https://avatars.a.example.com"))).toBe(true); 214 + }); 215 + 216 + it("should match hostname pattern with single asterisk at end", () => { 217 + const p = { hostname: pm.makeRe("avatars.ia*.example.com") } as const; 218 + expect(m(p, new URL("https://com"))).toBe(false); 219 + expect(m(p, new URL("https://example.com"))).toBe(false); 220 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 221 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 222 + expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); 223 + expect(m(p, new URL("https://avatars.example.com"))).toBe(false); 224 + expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(false); 225 + expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); 226 + expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(false); 227 + expect(m(p, new URL("https://avatars.sfo2.example.com"))).toBe(false); 228 + expect(m(p, new URL("https://avatars.iad2.example.com"))).toBe(true); 229 + expect(m(p, new URL("https://avatars.ia.example.com"))).toBe(true); 230 + }); 231 + 232 + it("should match hostname pattern with double asterisk", () => { 233 + const p = { hostname: pm.makeRe("**.example.com") } as const; 234 + expect(m(p, new URL("https://com"))).toBe(false); 235 + expect(m(p, new URL("https://example.com"))).toBe(false); 236 + expect(m(p, new URL("https://sub.example.com"))).toBe(true); 237 + expect(m(p, new URL("https://deep.sub.example.com"))).toBe(true); 238 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 239 + expect(m(p, new URL("https://sub.example.com.uk"))).toBe(false); 240 + expect(m(p, new URL("https://avatars.example.com"))).toBe(true); 241 + expect(m(p, new URL("https://avatars.sfo1.example.com"))).toBe(true); 242 + expect(m(p, new URL("https://avatars.iad1.example.com"))).toBe(true); 243 + expect(m(p, new URL("https://more.avatars.iad1.example.com"))).toBe(true); 244 + }); 245 + 246 + it("should match pathname pattern with single asterisk by itself", () => { 247 + const p = { 248 + hostname: pm.makeRe("example.com"), 249 + pathname: pm.makeRe("/act123/*/pic.jpg", { dot: true }), 250 + } as const; 251 + expect(m(p, new URL("https://com"))).toBe(false); 252 + expect(m(p, new URL("https://example.com"))).toBe(false); 253 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 254 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 255 + expect(m(p, new URL("https://example.com/act123"))).toBe(false); 256 + expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); 257 + expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); 258 + expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); 259 + expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); 260 + expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); 261 + expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); 262 + expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); 263 + expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); 264 + expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); 265 + expect(m(p, new URL("https://example.com/act123/team/usr4/pic.jpg"))).toBe(false); 266 + expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); 267 + }); 268 + 269 + it("should match pathname pattern with single asterisk at the beginning", () => { 270 + const p = { 271 + hostname: pm.makeRe("example.com"), 272 + pathname: pm.makeRe("/act123/*4/pic.jpg", { dot: true }), 273 + } as const; 274 + expect(m(p, new URL("https://com"))).toBe(false); 275 + expect(m(p, new URL("https://example.com"))).toBe(false); 276 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 277 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 278 + expect(m(p, new URL("https://example.com/act123"))).toBe(false); 279 + expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); 280 + expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); 281 + expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); 282 + expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); 283 + expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(false); 284 + expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(true); 285 + expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); 286 + expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); 287 + expect(m(p, new URL("https://example.com/act123/4/pic.jpg"))).toBe(true); 288 + }); 289 + 290 + it("should match pathname pattern with single asterisk in the middle", () => { 291 + const p = { 292 + hostname: pm.makeRe("example.com"), 293 + pathname: pm.makeRe("/act123/*sr*/pic.jpg", { dot: true }), 294 + } as const; 295 + expect(m(p, new URL("https://com"))).toBe(false); 296 + expect(m(p, new URL("https://example.com"))).toBe(false); 297 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 298 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 299 + expect(m(p, new URL("https://example.com/act123"))).toBe(false); 300 + expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); 301 + expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); 302 + expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); 303 + expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); 304 + expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); 305 + expect(m(p, new URL("https://example.com/act123/.sr6/pic.jpg"))).toBe(true); 306 + expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); 307 + expect(m(p, new URL("https://example.com/act123/team5/pic.jpg"))).toBe(false); 308 + expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); 309 + expect(m(p, new URL("https://example.com/act123/sr/pic.jpg"))).toBe(true); 310 + }); 311 + 312 + it("should match pathname pattern with single asterisk at the end", () => { 313 + const p = { 314 + hostname: pm.makeRe("example.com"), 315 + pathname: pm.makeRe("/act123/usr*/pic.jpg", { dot: true }), 316 + } as const; 317 + expect(m(p, new URL("https://com"))).toBe(false); 318 + expect(m(p, new URL("https://example.com"))).toBe(false); 319 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 320 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 321 + expect(m(p, new URL("https://example.com/act123"))).toBe(false); 322 + expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(false); 323 + expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(false); 324 + expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(false); 325 + expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); 326 + expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); 327 + expect(m(p, new URL("https://example.com/act123/usr/pic.jpg"))).toBe(true); 328 + expect(m(p, new URL("https://example.com/act123/team4/pic.jpg"))).toBe(false); 329 + expect(m(p, new URL("https://example.com/act456/team5/pic.jpg"))).toBe(false); 330 + expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); 331 + expect(m(p, new URL("https://sub.example.com/act123/usr6/pic.jpg"))).toBe(false); 332 + }); 333 + 334 + it("should match pathname pattern with double asterisk", () => { 335 + const p = { 336 + hostname: pm.makeRe("example.com"), 337 + pathname: pm.makeRe("/act123/**", { dot: true }), 338 + } as const; 339 + expect(m(p, new URL("https://com"))).toBe(false); 340 + expect(m(p, new URL("https://example.com"))).toBe(false); 341 + expect(m(p, new URL("https://sub.example.com"))).toBe(false); 342 + expect(m(p, new URL("https://example.com.uk"))).toBe(false); 343 + expect(m(p, new URL("https://example.com/act123"))).toBe(true); 344 + expect(m(p, new URL("https://example.com/act123/usr4"))).toBe(true); 345 + expect(m(p, new URL("https://example.com/act123/usr4/pic"))).toBe(true); 346 + expect(m(p, new URL("https://example.com/act123/usr4/picsjpg"))).toBe(true); 347 + expect(m(p, new URL("https://example.com/act123/usr4/pic.jpg"))).toBe(true); 348 + expect(m(p, new URL("https://example.com/act123/usr5/pic.jpg"))).toBe(true); 349 + expect(m(p, new URL("https://example.com/act123/usr6/pic.jpg"))).toBe(true); 350 + expect(m(p, new URL("https://example.com/act123/team/pic.jpg"))).toBe(true); 351 + expect(m(p, new URL("https://example.com/act123/.a/pic.jpg"))).toBe(true); 352 + expect(m(p, new URL("https://example.com/act123/team/.pic.jpg"))).toBe(true); 353 + expect(m(p, new URL("https://example.com/act456/team/pic.jpg"))).toBe(false); 354 + expect(m(p, new URL("https://example.com/team/pic.jpg"))).toBe(false); 355 + expect(m(p, new URL("https://sub.example.com/act123/team/pic.jpg"))).toBe(false); 356 + }); 357 + 358 + it("should throw when hostname is missing", () => { 359 + const p = { protocol: "https" } as const; 360 + // @ts-ignore testing invalid input 361 + expect(m(p, new URL("https://example.com"))).toBe(false); 362 + }); 363 + });
+98
packages/cloudflare/src/cli/templates/images.ts
··· 1 + export type RemotePattern = { 2 + protocol?: "http" | "https"; 3 + hostname: string; 4 + port?: string; 5 + // pathname is always set in the manifest (to `makeRe(pathname ?? '**', { dot: true }).source`) 6 + pathname: string; 7 + search?: string; 8 + }; 9 + 10 + /** 11 + * Fetches an images. 12 + * 13 + * Local images (starting with a '/' as fetched using the passed fetcher). 14 + * Remote images should match the configured remote patterns or a 404 response is returned. 15 + */ 16 + export function fetchImage(fetcher: Fetcher | undefined, imageUrl: string) { 17 + // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208 18 + if (!imageUrl || imageUrl.length > 3072 || imageUrl.startsWith("//")) { 19 + return getUrlErrorResponse(); 20 + } 21 + 22 + // Local 23 + if (imageUrl.startsWith("/")) { 24 + let pathname: string; 25 + try { 26 + const url = new URL(imageUrl, "http://n"); 27 + pathname = decodeURIComponent(url.pathname); 28 + } catch { 29 + return getUrlErrorResponse(); 30 + } 31 + if (/\/_next\/image($|\/)/.test(pathname)) { 32 + return getUrlErrorResponse(); 33 + } 34 + 35 + return fetcher?.fetch(`http://assets.local${imageUrl}`); 36 + } 37 + 38 + // Remote 39 + let url: URL; 40 + try { 41 + url = new URL(imageUrl); 42 + } catch { 43 + return getUrlErrorResponse(); 44 + } 45 + 46 + if (url.protocol !== "http:" && url.protocol !== "https:") { 47 + return getUrlErrorResponse(); 48 + } 49 + 50 + if (!__IMAGES_REMOTE_PATTERNS__.some((p: RemotePattern) => matchRemotePattern(p, url))) { 51 + return getUrlErrorResponse(); 52 + } 53 + 54 + return fetch(imageUrl, { cf: { cacheEverything: true } }); 55 + } 56 + 57 + export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { 58 + // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts 59 + if ( 60 + pattern.protocol !== undefined && 61 + pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "") 62 + ) { 63 + return false; 64 + } 65 + 66 + if (pattern.port !== undefined && pattern.port !== url.port) { 67 + return false; 68 + } 69 + 70 + if (pattern.hostname === undefined || !new RegExp(pattern.hostname).test(url.hostname)) { 71 + return false; 72 + } 73 + 74 + if (pattern.search !== undefined && pattern.search !== url.search) { 75 + return false; 76 + } 77 + 78 + // Should be the same as writeImagesManifest() 79 + if (!new RegExp(pattern.pathname).test(url.pathname)) { 80 + return false; 81 + } 82 + 83 + return true; 84 + } 85 + 86 + /** 87 + * @returns same error as Next.js when the url query parameter is not accepted. 88 + */ 89 + function getUrlErrorResponse() { 90 + return new Response(`"url" parameter is not allowed`, { status: 400 }); 91 + } 92 + 93 + /* eslint-disable no-var */ 94 + declare global { 95 + var __IMAGES_REMOTE_PATTERNS__: RemotePattern[]; 96 + var __IMAGES_LOCAL_PATTERNS__: unknown[]; 97 + } 98 + /* eslint-enable no-var */
-98
packages/cloudflare/src/cli/templates/init.ts
··· 140 140 process.env.__NEXT_PRIVATE_ORIGIN = url.origin; 141 141 } 142 142 143 - export type RemotePattern = { 144 - protocol?: "http" | "https"; 145 - hostname: string; 146 - port?: string; 147 - pathname: string; 148 - search?: string; 149 - }; 150 - 151 - const imgRemotePatterns = __IMAGES_REMOTE_PATTERNS__; 152 - 153 - /** 154 - * Fetches an images. 155 - * 156 - * Local images (starting with a '/' as fetched using the passed fetcher). 157 - * Remote images should match the configured remote patterns or a 404 response is returned. 158 - */ 159 - export function fetchImage(fetcher: Fetcher | undefined, url: string) { 160 - // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/server/image-optimizer.ts#L208 161 - if (!url || url.length > 3072 || url.startsWith("//")) { 162 - return new Response("Not Found", { status: 404 }); 163 - } 164 - 165 - // Local 166 - if (url.startsWith("/")) { 167 - if (/\/_next\/image($|\/)/.test(decodeURIComponent(parseUrl(url)?.pathname ?? ""))) { 168 - return new Response("Not Found", { status: 404 }); 169 - } 170 - 171 - return fetcher?.fetch(`http://assets.local${url}`); 172 - } 173 - 174 - // Remote 175 - let hrefParsed: URL; 176 - try { 177 - hrefParsed = new URL(url); 178 - } catch { 179 - return new Response("Not Found", { status: 404 }); 180 - } 181 - 182 - if (!["http:", "https:"].includes(hrefParsed.protocol)) { 183 - return new Response("Not Found", { status: 404 }); 184 - } 185 - 186 - if (!imgRemotePatterns.some((p: RemotePattern) => matchRemotePattern(p, hrefParsed))) { 187 - return new Response("Not Found", { status: 404 }); 188 - } 189 - 190 - return fetch(url, { cf: { cacheEverything: true } }); 191 - } 192 - 193 - export function matchRemotePattern(pattern: RemotePattern, url: URL): boolean { 194 - // https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts 195 - if (pattern.protocol !== undefined) { 196 - if (pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) { 197 - return false; 198 - } 199 - } 200 - if (pattern.port !== undefined) { 201 - if (pattern.port !== url.port) { 202 - return false; 203 - } 204 - } 205 - 206 - if (pattern.hostname === undefined) { 207 - throw new Error(`Pattern should define hostname but found\n${JSON.stringify(pattern)}`); 208 - } else { 209 - if (!new RegExp(pattern.hostname).test(url.hostname)) { 210 - return false; 211 - } 212 - } 213 - 214 - if (pattern.search !== undefined) { 215 - if (pattern.search !== url.search) { 216 - return false; 217 - } 218 - } 219 - 220 - // Should be the same as writeImagesManifest() 221 - if (!new RegExp(pattern.pathname).test(url.pathname)) { 222 - return false; 223 - } 224 - 225 - return true; 226 - } 227 - 228 - function parseUrl(url: string): URL | undefined { 229 - let parsed: URL | undefined = undefined; 230 - try { 231 - parsed = new URL(url, "http://n"); 232 - } catch { 233 - // empty 234 - } 235 - return parsed; 236 - } 237 - 238 143 /* eslint-disable no-var */ 239 144 declare global { 240 145 // Build timestamp 241 146 var __BUILD_TIMESTAMP_MS__: number; 242 147 // Next basePath 243 148 var __NEXT_BASE_PATH__: string; 244 - // Images patterns 245 - var __IMAGES_REMOTE_PATTERNS__: RemotePattern[]; 246 - var __IMAGES_LOCAL_PATTERNS__: unknown[]; 247 149 } 248 150 /* eslint-enable no-var */
+3 -1
packages/cloudflare/src/cli/templates/worker.ts
··· 1 1 //@ts-expect-error: Will be resolved by wrangler build 2 - import { fetchImage, runWithCloudflareRequestContext } from "./cloudflare/init.js"; 2 + import { fetchImage } from "./cloudflare/images.js"; 3 + //@ts-expect-error: Will be resolved by wrangler build 4 + import { runWithCloudflareRequestContext } from "./cloudflare/init.js"; 3 5 // @ts-expect-error: Will be resolved by wrangler build 4 6 import { handler as middlewareHandler } from "./middleware/handler.mjs"; 5 7
+2 -1
packages/cloudflare/tsconfig.json
··· 15 15 "target": "ES2022", 16 16 "types": ["@cloudflare/workers-types", "@opennextjs/aws/types/global.d.ts"] 17 17 }, 18 - "include": ["src/**/*.ts", "env.d.ts"] 18 + "include": ["src/**/*.ts", "env.d.ts"], 19 + "exclude": ["src/**/*.spec.ts"] 19 20 }