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 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, + }; + }, + ); }, }; } diff --git a/packages/open-next/src/utils/regex.ts b/packages/open-next/src/utils/regex.ts new file mode 100644 index 000000000..37c00dfcb --- /dev/null +++ b/packages/open-next/src/utils/regex.ts @@ -0,0 +1,27 @@ +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. + * - Special characters are escaped by default but can be controlled through opts.escape. + * - Posix separators are always escaped. + * + * @example + * ```ts + * getCrossPlatformPathRegex("./middleware.mjs") + * getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false }) + * ``` + */ +export function getCrossPlatformPathRegex( + regex: string, + { escape: shouldEscape = true, flags = "g" }: Options = {}, +) { + const newExpr = ( + shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex + ).replaceAll("/", String.raw`(?:\/|\\)`); + + return new RegExp(newExpr, flags); +} 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..7c322a436 --- /dev/null +++ b/packages/tests-unit/tests/utils/regex.test.ts @@ -0,0 +1,41 @@ +import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js"; + +const specialChars = "^([123]+|[123]{1,3})*\\?$"; + +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(String.raw`test(?:\/|\\)path`); + }); + }); + + it("should return a regex with escaped characters", () => { + const regexp = getCrossPlatformPathRegex(specialChars, { escape: true }); + expect(regexp.source).toEqual( + 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( + String.raw`\./middleware\.(mjs|cjs)`, + { escape: false }, + ); + expect(regex.source).toEqual(String.raw`\.(?:\/|\\)middleware\.(mjs|cjs)`); + }); +});