From 6a6eadb3613093c7ce83548b8cfc57ed4454984b Mon Sep 17 00:00:00 2001 From: James Date: Thu, 16 Jan 2025 15:55:01 +0000 Subject: [PATCH 01/11] create utility --- packages/open-next/src/utils/regex.ts | 10 +++++++++ packages/tests-unit/tests/utils/regex.test.ts | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/open-next/src/utils/regex.ts create mode 100644 packages/tests-unit/tests/utils/regex.test.ts diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts new file mode 100644 index 000000000..fc8808715 --- /dev/null +++ b/packages/open-next/src/utils/regex.ts @@ -0,0 +1,10 @@ +export function getCrossPlatformPathRegex( + regex: string, + opts: { escape: boolean } = { escape: true }, +) { + const newExpr = ( + opts.escape ? regex.replace(/([[\]().*+?^$|])/g, '\\$1') : regex + ).replaceAll('/', '(?:\\/|\\\\)'); + + return new RegExp(newExpr, 'g'); +} diff --git a/packages/tests-unit/tests/utils/regex.test.ts b/packages/tests-unit/tests/utils/regex.test.ts new file mode 100644 index 000000000..21df1a1cb --- /dev/null +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -0,0 +1,22 @@ +import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; + +const specialChars = "^([123]+|[123]*)?$"; + +describe("getCrossPlatformPathRegex", () => { + it("should return a regex without escaping characters", () => { + const regexp = getCrossPlatformPathRegex(specialChars, { escape: false }); + expect(regexp.source).toEqual(specialChars); + }); + + it("should always create cross-platform separators", () => { + [true, false].forEach((v) => { + const regexp = getCrossPlatformPathRegex("test/path", { escape: v }); + expect(regexp.source).toEqual("test(?:\\/|\\\\)path"); + }); + }); + + it("should return a regex with escaped characters", () => { + const regexp = getCrossPlatformPathRegex(specialChars, { escape: true }); + expect(regexp.source).toEqual("\\^\\(\\[123\\]\\+\\|\\[123\\]\\*\\)\\?\\$"); + }); +}); From d1e63c55eb1f1fa58c66a464661a745ae49a860f Mon Sep 17 00:00:00 2001 From: James Date: Thu, 16 Jan 2025 15:55:18 +0000 Subject: [PATCH 02/11] replace usage with the utility --- .../build/createImageOptimizationBundle.ts | 6 ++- .../open-next/src/build/createServerBundle.ts | 5 +- .../src/build/edge/createEdgeBundle.ts | 3 +- packages/open-next/src/plugins/edge.ts | 18 ++++--- packages/open-next/src/plugins/resolve.ts | 52 ++++++++++--------- 5 files changed, 48 insertions(+), 36 deletions(-) diff --git a/packages/open-next/src/build/createImageOptimizationBundle.ts b/packages/open-next/src/build/createImageOptimizationBundle.ts index 1636f8d15..5686d4bda 100644 --- a/packages/open-next/src/build/createImageOptimizationBundle.ts +++ b/packages/open-next/src/build/createImageOptimizationBundle.ts @@ -5,6 +5,7 @@ import path from "node:path"; import logger from "../logger.js"; import { openNextReplacementPlugin } from "../plugins/replacement.js"; import { openNextResolvePlugin } from "../plugins/resolve.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; import * as buildHelper from "./helper.js"; import { installDependencies } from "./installDeps.js"; @@ -39,8 +40,9 @@ export async function createImageOptimizationBundle( plugins.push( openNextReplacementPlugin({ name: "opennext-14.1.1-image-optimization", - target: - /plugins(\/|\\)image-optimization(\/|\\)image-optimization\.js/g, + target: getCrossPlatformPathRegex( + "plugins/image-optimization/image-optimization.js", + ), replacements: [ require.resolve( "../adapters/plugins/image-optimization/image-optimization.replacement.js", diff --git a/packages/open-next/src/build/createServerBundle.ts b/packages/open-next/src/build/createServerBundle.ts index e5de80540..67fcdc879 100644 --- a/packages/open-next/src/build/createServerBundle.ts +++ b/packages/open-next/src/build/createServerBundle.ts @@ -7,6 +7,7 @@ import logger from "../logger.js"; import { minifyAll } from "../minimize-js.js"; import { openNextReplacementPlugin } from "../plugins/replacement.js"; import { openNextResolvePlugin } from "../plugins/resolve.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; import { bundleNextServer } from "./bundleNextServer.js"; import { compileCache } from "./compileCache.js"; import { copyTracedFiles } from "./copyTracedFiles.js"; @@ -185,7 +186,7 @@ async function generateBundle( const plugins = [ openNextReplacementPlugin({ name: `requestHandlerOverride ${name}`, - target: /core(\/|\\)requestHandler\.js/g, + target: getCrossPlatformPathRegex("core/requestHandler.js"), deletes: [ ...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []), ...(disableRouting ? ["withRouting"] : []), @@ -193,7 +194,7 @@ async function generateBundle( }), openNextReplacementPlugin({ name: `utilOverride ${name}`, - target: /core(\/|\\)util\.js/g, + target: getCrossPlatformPathRegex("core/util.js"), deletes: [ ...(disableNextPrebundledReact ? ["requireHooks"] : []), ...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]), diff --git a/packages/open-next/src/build/edge/createEdgeBundle.ts b/packages/open-next/src/build/edge/createEdgeBundle.ts index 28c05ef07..a3664116d 100644 --- a/packages/open-next/src/build/edge/createEdgeBundle.ts +++ b/packages/open-next/src/build/edge/createEdgeBundle.ts @@ -18,6 +18,7 @@ import logger from "../../logger.js"; import { openNextEdgePlugins } from "../../plugins/edge.js"; import { openNextReplacementPlugin } from "../../plugins/replacement.js"; import { openNextResolvePlugin } from "../../plugins/resolve.js"; +import { getCrossPlatformPathRegex } from "../../utils/regex.js"; import { type BuildOptions, isEdgeRuntime } from "../helper.js"; import { copyOpenNextConfig, esbuildAsync } from "../helper.js"; @@ -84,7 +85,7 @@ export async function buildEdgeBundle({ }), openNextReplacementPlugin({ name: "externalMiddlewareOverrides", - target: /adapters(\/|\\)middleware\.js/g, + target: getCrossPlatformPathRegex("adapters/middleware.js"), deletes: includeCache ? [] : ["includeCacheInMiddleware"], }), openNextEdgePlugins({ diff --git a/packages/open-next/src/plugins/edge.ts b/packages/open-next/src/plugins/edge.ts index c31d970ce..284b8aa32 100644 --- a/packages/open-next/src/plugins/edge.ts +++ b/packages/open-next/src/plugins/edge.ts @@ -18,6 +18,7 @@ import { loadRoutesManifest, } from "../adapters/config/util.js"; import logger from "../logger.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; export interface IPluginSettings { nextDir: string; @@ -58,11 +59,14 @@ export function openNextEdgePlugins({ logger.debug(chalk.blue("OpenNext Edge plugin")); if (edgeFunctionHandlerPath) { // If we bundle the routing, we need to resolve the middleware - build.onResolve({ filter: /\.(\/|\\)middleware.mjs/g }, () => { - return { - path: edgeFunctionHandlerPath, - }; - }); + build.onResolve( + { filter: getCrossPlatformPathRegex("./middleware.mjs") }, + () => { + return { + path: edgeFunctionHandlerPath, + }; + }, + ); } build.onResolve({ filter: /\.(mjs|wasm)$/g }, () => { @@ -94,7 +98,7 @@ export function openNextEdgePlugins({ // We inject the entry files into the edgeFunctionHandler build.onLoad( - { filter: /(\/|\\)edgeFunctionHandler.js/g }, + { filter: getCrossPlatformPathRegex("/edgeFunctionHandler.js") }, async (args) => { let contents = readFileSync(args.path, "utf-8"); contents = ` @@ -164,7 +168,7 @@ ${contents} ); build.onLoad( - { filter: /adapters(\/|\\)config(\/|\\)index/g }, + { filter: getCrossPlatformPathRegex("adapters/config/index") }, async () => { const NextConfig = loadConfig(nextDir); const BuildId = loadBuildId(nextDir); diff --git a/packages/open-next/src/plugins/resolve.ts b/packages/open-next/src/plugins/resolve.ts index 5630f1cf3..23fe6e7b4 100644 --- a/packages/open-next/src/plugins/resolve.ts +++ b/packages/open-next/src/plugins/resolve.ts @@ -13,6 +13,7 @@ import type { import type { ImageLoader, OriginResolver, Warmer } from "types/overrides"; import logger from "../logger.js"; +import { getCrossPlatformPathRegex } from "../utils/regex.js"; export interface IPluginSettings { overrides?: { @@ -81,31 +82,34 @@ export function openNextResolvePlugin({ chalk.blue("OpenNext Resolve plugin"), fnName ? `for ${fnName}` : "", ); - build.onLoad({ filter: /core(\/|\\)resolve\.js/g }, async (args) => { - let contents = readFileSync(args.path, "utf-8"); - const overridesEntries = Object.entries(overrides ?? {}); - for (let [overrideName, overrideValue] of overridesEntries) { - if (!overrideValue) { - continue; - } - if (overrideName === "wrapper" && overrideValue === "cloudflare") { - // "cloudflare" is deprecated and replaced by "cloudflare-edge". - overrideValue = "cloudflare-edge"; - } - const folder = - nameToFolder[overrideName as keyof typeof nameToFolder]; - const defaultOverride = - defaultOverrides[overrideName as keyof typeof defaultOverrides]; + build.onLoad( + { filter: getCrossPlatformPathRegex("core/resolve.js") }, + async (args) => { + let contents = readFileSync(args.path, "utf-8"); + const overridesEntries = Object.entries(overrides ?? {}); + for (let [overrideName, overrideValue] of overridesEntries) { + if (!overrideValue) { + continue; + } + if (overrideName === "wrapper" && overrideValue === "cloudflare") { + // "cloudflare" is deprecated and replaced by "cloudflare-edge". + overrideValue = "cloudflare-edge"; + } + const folder = + nameToFolder[overrideName as keyof typeof nameToFolder]; + const defaultOverride = + defaultOverrides[overrideName as keyof typeof defaultOverrides]; - contents = contents.replace( - `../overrides/${folder}/${defaultOverride}.js`, - `../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js`, - ); - } - return { - contents, - }; - }); + contents = contents.replace( + `../overrides/${folder}/${defaultOverride}.js`, + `../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js`, + ); + } + return { + contents, + }; + }, + ); }, }; } From cef92ca760968e5599af8213939bbca4e87897d4 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 16 Jan 2025 15:55:27 +0000 Subject: [PATCH 03/11] changeset --- .changeset/hungry-geckos-knock.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/hungry-geckos-knock.md diff --git a/.changeset/hungry-geckos-knock.md b/.changeset/hungry-geckos-knock.md new file mode 100644 index 000000000..4a83539b6 --- /dev/null +++ b/.changeset/hungry-geckos-knock.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +refactor: use utility for cross-platform path regex construction From 3380a0760057cd8648f2386820f88f3e9c802d96 Mon Sep 17 00:00:00 2001 From: James Date: Thu, 16 Jan 2025 15:58:49 +0000 Subject: [PATCH 04/11] fix formatting --- packages/open-next/src/utils/regex.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index fc8808715..98c7e9302 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -3,8 +3,8 @@ export function getCrossPlatformPathRegex( opts: { escape: boolean } = { escape: true }, ) { const newExpr = ( - opts.escape ? regex.replace(/([[\]().*+?^$|])/g, '\\$1') : regex - ).replaceAll('/', '(?:\\/|\\\\)'); + opts.escape ? regex.replace(/([[\]().*+?^$|])/g, "\\$1") : regex + ).replaceAll("/", "(?:\\/|\\\\)"); - return new RegExp(newExpr, 'g'); + return new RegExp(newExpr, "g"); } From b71a91d2a03074599293584dfdf68e3877be47dc Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 08:32:02 +0000 Subject: [PATCH 05/11] add comment --- packages/open-next/src/utils/regex.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index 98c7e9302..1149e8cb3 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -1,3 +1,14 @@ +/** + * Constructs a regular expression for a path that supports separators for multiple platforms + * - Uses posix separators (`/`) as the input that should be made cross-platform. + * - Special characters are escaped by default but can be controlled through opts.escape. + * + * @example + * ```ts + * getCrossPlatformPathRegex("./middleware.mjs") + * getCrossPlatformPathRegex("\\./middleware\\.(mjs|cjs)", { escape: false }) + * ``` + */ export function getCrossPlatformPathRegex( regex: string, opts: { escape: boolean } = { escape: true }, From f1255528578dd1274bc4254d369fd7976777ddfb Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 08:33:01 +0000 Subject: [PATCH 06/11] add some additional special chars and use string.raw --- packages/open-next/src/utils/regex.ts | 4 ++-- packages/tests-unit/tests/utils/regex.test.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index 1149e8cb3..5c0954ccc 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -14,8 +14,8 @@ export function getCrossPlatformPathRegex( opts: { escape: boolean } = { escape: true }, ) { const newExpr = ( - opts.escape ? regex.replace(/([[\]().*+?^$|])/g, "\\$1") : regex - ).replaceAll("/", "(?:\\/|\\\\)"); + opts.escape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex + ).replaceAll("/", String.raw`(?:\/|\\)`); return new RegExp(newExpr, "g"); } diff --git a/packages/tests-unit/tests/utils/regex.test.ts b/packages/tests-unit/tests/utils/regex.test.ts index 21df1a1cb..9b054b31a 100644 --- a/packages/tests-unit/tests/utils/regex.test.ts +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -1,6 +1,6 @@ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; -const specialChars = "^([123]+|[123]*)?$"; +const specialChars = "^([123]+|[123]{1,3})*\\?$"; describe("getCrossPlatformPathRegex", () => { it("should return a regex without escaping characters", () => { @@ -11,12 +11,14 @@ describe("getCrossPlatformPathRegex", () => { it("should always create cross-platform separators", () => { [true, false].forEach((v) => { const regexp = getCrossPlatformPathRegex("test/path", { escape: v }); - expect(regexp.source).toEqual("test(?:\\/|\\\\)path"); + expect(regexp.source).toEqual(String.raw`test(?:\/|\\)path`); }); }); it("should return a regex with escaped characters", () => { const regexp = getCrossPlatformPathRegex(specialChars, { escape: true }); - expect(regexp.source).toEqual("\\^\\(\\[123\\]\\+\\|\\[123\\]\\*\\)\\?\\$"); + expect(regexp.source).toEqual( + String.raw`\^\(\[123\]\+\|\[123\]\{1,3\}\)\*\\\?\$`, + ); }); }); From 3a5c6fd5a7230ed017e248195a9f778cf4a6c76f Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 08:33:36 +0000 Subject: [PATCH 07/11] add extra tests --- packages/tests-unit/tests/utils/regex.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/tests-unit/tests/utils/regex.test.ts b/packages/tests-unit/tests/utils/regex.test.ts index 9b054b31a..4b3088578 100644 --- a/packages/tests-unit/tests/utils/regex.test.ts +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -21,4 +21,20 @@ describe("getCrossPlatformPathRegex", () => { String.raw`\^\(\[123\]\+\|\[123\]\{1,3\}\)\*\\\?\$`, ); }); + + it("should return cross-platform paths with escaped special characters", () => { + [ + ["core/resolve.js", String.raw`core(?:\/|\\)resolve\.js`], + ["./middleware.mjs", String.raw`\.(?:\/|\\)middleware\.mjs`], + ].forEach(([input, output]) => + expect(getCrossPlatformPathRegex(input).source).toEqual(output), + ); + }); + + it("should return cross-platform paths without escaping special characters", () => { + const regex = getCrossPlatformPathRegex("\\./middleware\\.(mjs|cjs)", { + escape: false, + }); + expect(regex.source).toEqual(String.raw`\.(?:\/|\\)middleware\.(mjs|cjs)`); + }); }); From b6bc25ddf4d15a99125215eb4bdf7f2d2ff44d60 Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 09:05:33 +0000 Subject: [PATCH 08/11] add another string.raw --- packages/tests-unit/tests/utils/regex.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/tests-unit/tests/utils/regex.test.ts b/packages/tests-unit/tests/utils/regex.test.ts index 4b3088578..7c322a436 100644 --- a/packages/tests-unit/tests/utils/regex.test.ts +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -32,9 +32,10 @@ describe("getCrossPlatformPathRegex", () => { }); it("should return cross-platform paths without escaping special characters", () => { - const regex = getCrossPlatformPathRegex("\\./middleware\\.(mjs|cjs)", { - escape: false, - }); + const regex = getCrossPlatformPathRegex( + String.raw`\./middleware\.(mjs|cjs)`, + { escape: false }, + ); expect(regex.source).toEqual(String.raw`\.(?:\/|\\)middleware\.(mjs|cjs)`); }); }); From 76554cc7b72cae50a48442ceb4b870430b053d8b Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 09:07:55 +0000 Subject: [PATCH 09/11] use string.raw in comment example too --- packages/open-next/src/utils/regex.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index 5c0954ccc..d2336728b 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -6,7 +6,7 @@ * @example * ```ts * getCrossPlatformPathRegex("./middleware.mjs") - * getCrossPlatformPathRegex("\\./middleware\\.(mjs|cjs)", { escape: false }) + * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false }) * ``` */ export function getCrossPlatformPathRegex( From f46f4bdaa294537f3ba581045cd80f4ad2dcef43 Mon Sep 17 00:00:00 2001 From: James Anderson Date: Fri, 17 Jan 2025 10:04:48 +0000 Subject: [PATCH 10/11] Update packages/open-next/src/utils/regex.ts --- packages/open-next/src/utils/regex.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index d2336728b..239ed93fc 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -2,6 +2,7 @@ * Constructs a regular expression for a path that supports separators for multiple platforms * - Uses posix separators (`/`) as the input that should be made cross-platform. * - Special characters are escaped by default but can be controlled through opts.escape. + * - Posix separators are always escaped. * * @example * ```ts From 8d603aecb4bd255b84bd53a6d053b0a0e7a5788b Mon Sep 17 00:00:00 2001 From: James Date: Fri, 17 Jan 2025 11:28:59 +0000 Subject: [PATCH 11/11] change options --- packages/open-next/src/utils/regex.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts index 239ed93fc..37c00dfcb 100644 --- a/packages/open-next/src/utils/regex.ts +++ b/packages/open-next/src/utils/regex.ts @@ -1,3 +1,8 @@ +type Options = { + escape?: boolean; + flags?: string; +}; + /** * Constructs a regular expression for a path that supports separators for multiple platforms * - Uses posix separators (`/`) as the input that should be made cross-platform. @@ -12,11 +17,11 @@ */ export function getCrossPlatformPathRegex( regex: string, - opts: { escape: boolean } = { escape: true }, + { escape: shouldEscape = true, flags = "g" }: Options = {}, ) { const newExpr = ( - opts.escape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex + shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex ).replaceAll("/", String.raw`(?:\/|\\)`); - return new RegExp(newExpr, "g"); + return new RegExp(newExpr, flags); }