Skip to content

Commit 00ce837

Browse files
authored
refactor: use utility for cross-platform path regex construction (#701)
* create utility * replace usage with the utility * changeset * fix formatting * add comment * add some additional special chars and use string.raw * add extra tests * add another string.raw * use string.raw in comment example too * Update packages/open-next/src/utils/regex.ts * change options
1 parent 25d317c commit 00ce837

File tree

8 files changed

+121
-36
lines changed

8 files changed

+121
-36
lines changed

.changeset/hungry-geckos-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
refactor: use utility for cross-platform path regex construction

packages/open-next/src/build/createImageOptimizationBundle.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import path from "node:path";
55
import logger from "../logger.js";
66
import { openNextReplacementPlugin } from "../plugins/replacement.js";
77
import { openNextResolvePlugin } from "../plugins/resolve.js";
8+
import { getCrossPlatformPathRegex } from "../utils/regex.js";
89
import * as buildHelper from "./helper.js";
910
import { installDependencies } from "./installDeps.js";
1011

@@ -39,8 +40,9 @@ export async function createImageOptimizationBundle(
3940
plugins.push(
4041
openNextReplacementPlugin({
4142
name: "opennext-14.1.1-image-optimization",
42-
target:
43-
/plugins(\/|\\)image-optimization(\/|\\)image-optimization\.js/g,
43+
target: getCrossPlatformPathRegex(
44+
"plugins/image-optimization/image-optimization.js",
45+
),
4446
replacements: [
4547
require.resolve(
4648
"../adapters/plugins/image-optimization/image-optimization.replacement.js",

packages/open-next/src/build/createServerBundle.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import logger from "../logger.js";
77
import { minifyAll } from "../minimize-js.js";
88
import { openNextReplacementPlugin } from "../plugins/replacement.js";
99
import { openNextResolvePlugin } from "../plugins/resolve.js";
10+
import { getCrossPlatformPathRegex } from "../utils/regex.js";
1011
import { bundleNextServer } from "./bundleNextServer.js";
1112
import { compileCache } from "./compileCache.js";
1213
import { copyTracedFiles } from "./copyTracedFiles.js";
@@ -185,15 +186,15 @@ async function generateBundle(
185186
const plugins = [
186187
openNextReplacementPlugin({
187188
name: `requestHandlerOverride ${name}`,
188-
target: /core(\/|\\)requestHandler\.js/g,
189+
target: getCrossPlatformPathRegex("core/requestHandler.js"),
189190
deletes: [
190191
...(disableNextPrebundledReact ? ["applyNextjsPrebundledReact"] : []),
191192
...(disableRouting ? ["withRouting"] : []),
192193
],
193194
}),
194195
openNextReplacementPlugin({
195196
name: `utilOverride ${name}`,
196-
target: /core(\/|\\)util\.js/g,
197+
target: getCrossPlatformPathRegex("core/util.js"),
197198
deletes: [
198199
...(disableNextPrebundledReact ? ["requireHooks"] : []),
199200
...(isBefore13413 ? ["trustHostHeader"] : ["requestHandlerHost"]),

packages/open-next/src/build/edge/createEdgeBundle.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import logger from "../../logger.js";
1818
import { openNextEdgePlugins } from "../../plugins/edge.js";
1919
import { openNextReplacementPlugin } from "../../plugins/replacement.js";
2020
import { openNextResolvePlugin } from "../../plugins/resolve.js";
21+
import { getCrossPlatformPathRegex } from "../../utils/regex.js";
2122
import { type BuildOptions, isEdgeRuntime } from "../helper.js";
2223
import { copyOpenNextConfig, esbuildAsync } from "../helper.js";
2324

@@ -84,7 +85,7 @@ export async function buildEdgeBundle({
8485
}),
8586
openNextReplacementPlugin({
8687
name: "externalMiddlewareOverrides",
87-
target: /adapters(\/|\\)middleware\.js/g,
88+
target: getCrossPlatformPathRegex("adapters/middleware.js"),
8889
deletes: includeCache ? [] : ["includeCacheInMiddleware"],
8990
}),
9091
openNextEdgePlugins({

packages/open-next/src/plugins/edge.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
loadRoutesManifest,
1919
} from "../adapters/config/util.js";
2020
import logger from "../logger.js";
21+
import { getCrossPlatformPathRegex } from "../utils/regex.js";
2122

2223
export interface IPluginSettings {
2324
nextDir: string;
@@ -58,11 +59,14 @@ export function openNextEdgePlugins({
5859
logger.debug(chalk.blue("OpenNext Edge plugin"));
5960
if (edgeFunctionHandlerPath) {
6061
// If we bundle the routing, we need to resolve the middleware
61-
build.onResolve({ filter: /\.(\/|\\)middleware.mjs/g }, () => {
62-
return {
63-
path: edgeFunctionHandlerPath,
64-
};
65-
});
62+
build.onResolve(
63+
{ filter: getCrossPlatformPathRegex("./middleware.mjs") },
64+
() => {
65+
return {
66+
path: edgeFunctionHandlerPath,
67+
};
68+
},
69+
);
6670
}
6771

6872
build.onResolve({ filter: /\.(mjs|wasm)$/g }, () => {
@@ -94,7 +98,7 @@ export function openNextEdgePlugins({
9498

9599
// We inject the entry files into the edgeFunctionHandler
96100
build.onLoad(
97-
{ filter: /(\/|\\)edgeFunctionHandler.js/g },
101+
{ filter: getCrossPlatformPathRegex("/edgeFunctionHandler.js") },
98102
async (args) => {
99103
let contents = readFileSync(args.path, "utf-8");
100104
contents = `
@@ -164,7 +168,7 @@ ${contents}
164168
);
165169

166170
build.onLoad(
167-
{ filter: /adapters(\/|\\)config(\/|\\)index/g },
171+
{ filter: getCrossPlatformPathRegex("adapters/config/index") },
168172
async () => {
169173
const NextConfig = loadConfig(nextDir);
170174
const BuildId = loadBuildId(nextDir);

packages/open-next/src/plugins/resolve.ts

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
import type { ImageLoader, OriginResolver, Warmer } from "types/overrides";
1414

1515
import logger from "../logger.js";
16+
import { getCrossPlatformPathRegex } from "../utils/regex.js";
1617

1718
export interface IPluginSettings {
1819
overrides?: {
@@ -81,31 +82,34 @@ export function openNextResolvePlugin({
8182
chalk.blue("OpenNext Resolve plugin"),
8283
fnName ? `for ${fnName}` : "",
8384
);
84-
build.onLoad({ filter: /core(\/|\\)resolve\.js/g }, async (args) => {
85-
let contents = readFileSync(args.path, "utf-8");
86-
const overridesEntries = Object.entries(overrides ?? {});
87-
for (let [overrideName, overrideValue] of overridesEntries) {
88-
if (!overrideValue) {
89-
continue;
90-
}
91-
if (overrideName === "wrapper" && overrideValue === "cloudflare") {
92-
// "cloudflare" is deprecated and replaced by "cloudflare-edge".
93-
overrideValue = "cloudflare-edge";
94-
}
95-
const folder =
96-
nameToFolder[overrideName as keyof typeof nameToFolder];
97-
const defaultOverride =
98-
defaultOverrides[overrideName as keyof typeof defaultOverrides];
85+
build.onLoad(
86+
{ filter: getCrossPlatformPathRegex("core/resolve.js") },
87+
async (args) => {
88+
let contents = readFileSync(args.path, "utf-8");
89+
const overridesEntries = Object.entries(overrides ?? {});
90+
for (let [overrideName, overrideValue] of overridesEntries) {
91+
if (!overrideValue) {
92+
continue;
93+
}
94+
if (overrideName === "wrapper" && overrideValue === "cloudflare") {
95+
// "cloudflare" is deprecated and replaced by "cloudflare-edge".
96+
overrideValue = "cloudflare-edge";
97+
}
98+
const folder =
99+
nameToFolder[overrideName as keyof typeof nameToFolder];
100+
const defaultOverride =
101+
defaultOverrides[overrideName as keyof typeof defaultOverrides];
99102

100-
contents = contents.replace(
101-
`../overrides/${folder}/${defaultOverride}.js`,
102-
`../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js`,
103-
);
104-
}
105-
return {
106-
contents,
107-
};
108-
});
103+
contents = contents.replace(
104+
`../overrides/${folder}/${defaultOverride}.js`,
105+
`../overrides/${folder}/${getOverrideOrDummy(overrideValue)}.js`,
106+
);
107+
}
108+
return {
109+
contents,
110+
};
111+
},
112+
);
109113
},
110114
};
111115
}

packages/open-next/src/utils/regex.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
type Options = {
2+
escape?: boolean;
3+
flags?: string;
4+
};
5+
6+
/**
7+
* Constructs a regular expression for a path that supports separators for multiple platforms
8+
* - Uses posix separators (`/`) as the input that should be made cross-platform.
9+
* - Special characters are escaped by default but can be controlled through opts.escape.
10+
* - Posix separators are always escaped.
11+
*
12+
* @example
13+
* ```ts
14+
* getCrossPlatformPathRegex("./middleware.mjs")
15+
* getCrossPlatformPathRegex(String.raw`\./middleware\.(mjs|cjs)`, { escape: false })
16+
* ```
17+
*/
18+
export function getCrossPlatformPathRegex(
19+
regex: string,
20+
{ escape: shouldEscape = true, flags = "g" }: Options = {},
21+
) {
22+
const newExpr = (
23+
shouldEscape ? regex.replace(/([[\]().*+?^$|{}\\])/g, "\\$1") : regex
24+
).replaceAll("/", String.raw`(?:\/|\\)`);
25+
26+
return new RegExp(newExpr, flags);
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
2+
3+
const specialChars = "^([123]+|[123]{1,3})*\\?$";
4+
5+
describe("getCrossPlatformPathRegex", () => {
6+
it("should return a regex without escaping characters", () => {
7+
const regexp = getCrossPlatformPathRegex(specialChars, { escape: false });
8+
expect(regexp.source).toEqual(specialChars);
9+
});
10+
11+
it("should always create cross-platform separators", () => {
12+
[true, false].forEach((v) => {
13+
const regexp = getCrossPlatformPathRegex("test/path", { escape: v });
14+
expect(regexp.source).toEqual(String.raw`test(?:\/|\\)path`);
15+
});
16+
});
17+
18+
it("should return a regex with escaped characters", () => {
19+
const regexp = getCrossPlatformPathRegex(specialChars, { escape: true });
20+
expect(regexp.source).toEqual(
21+
String.raw`\^\(\[123\]\+\|\[123\]\{1,3\}\)\*\\\?\$`,
22+
);
23+
});
24+
25+
it("should return cross-platform paths with escaped special characters", () => {
26+
[
27+
["core/resolve.js", String.raw`core(?:\/|\\)resolve\.js`],
28+
["./middleware.mjs", String.raw`\.(?:\/|\\)middleware\.mjs`],
29+
].forEach(([input, output]) =>
30+
expect(getCrossPlatformPathRegex(input).source).toEqual(output),
31+
);
32+
});
33+
34+
it("should return cross-platform paths without escaping special characters", () => {
35+
const regex = getCrossPlatformPathRegex(
36+
String.raw`\./middleware\.(mjs|cjs)`,
37+
{ escape: false },
38+
);
39+
expect(regex.source).toEqual(String.raw`\.(?:\/|\\)middleware\.(mjs|cjs)`);
40+
});
41+
});

0 commit comments

Comments
 (0)