Skip to content

Commit 5c90521

Browse files
authored
refactor: Make the list of optional dependencies configurable (#297)
1 parent 1b3a972 commit 5c90521

File tree

5 files changed

+120
-176
lines changed

5 files changed

+120
-176
lines changed

.changeset/fresh-walls-hug.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/cloudflare": patch
3+
---
4+
5+
refactor: Make the list of optional dependencies configurable

packages/cloudflare/src/cli/build/bundle-server.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,20 @@ import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
1717
/** The dist directory of the Cloudflare adapter package */
1818
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
1919

20+
/**
21+
* List of optional Next.js dependencies.
22+
* They are not required for Next.js to run but only needed to enabled specific features.
23+
* When one of those dependency is required, it should be installed by the application.
24+
*/
25+
const optionalDependencies = [
26+
"caniuse-lite",
27+
"critters",
28+
"jimp",
29+
"probe-image-size",
30+
// `server.edge` is not available in react-dom@18
31+
"react-dom/server.edge",
32+
];
33+
2034
/**
2135
* Bundle the Open Next server.
2236
*/
@@ -56,13 +70,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
5670
inlineRequirePagePlugin(buildOpts),
5771
setWranglerExternal(),
5872
],
59-
external: [
60-
"./middleware/handler.mjs",
61-
// Next optional dependencies.
62-
"caniuse-lite",
63-
"jimp",
64-
"probe-image-size",
65-
],
73+
external: ["./middleware/handler.mjs", ...optionalDependencies],
6674
alias: {
6775
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
6876
// eval("require")("bufferutil");
@@ -196,7 +204,7 @@ async function updateWorkerBundledCode(workerOutputFile: string, buildOpts: Buil
196204

197205
const bundle = parse(Lang.TypeScript, patchedCode).root();
198206

199-
const { edits } = patchOptionalDependencies(bundle);
207+
const { edits } = patchOptionalDependencies(bundle, optionalDependencies);
200208

201209
await writeFile(workerOutputFile, bundle.commitEdits(edits));
202210
}

packages/cloudflare/src/cli/build/patches/ast/optional-deps.spec.ts

+66-14
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { describe, expect, it } from "vitest";
22

3-
import { optionalDepRule } from "./optional-deps.js";
3+
import { buildOptionalDepRule } from "./optional-deps.js";
44
import { patchCode } from "./util.js";
55

66
describe("optional dependecy", () => {
77
it('should wrap a top-level require("caniuse-lite") in a try-catch', () => {
88
const code = `t = require("caniuse-lite");`;
9-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
9+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
1010
"try {
1111
t = require("caniuse-lite");
1212
} catch {
@@ -17,7 +17,7 @@ describe("optional dependecy", () => {
1717

1818
it('should wrap a top-level require("caniuse-lite/data") in a try-catch', () => {
1919
const code = `t = require("caniuse-lite/data");`;
20-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(
20+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(
2121
`
2222
"try {
2323
t = require("caniuse-lite/data");
@@ -30,7 +30,7 @@ describe("optional dependecy", () => {
3030

3131
it('should wrap e.exports = require("caniuse-lite") in a try-catch', () => {
3232
const code = 'e.exports = require("caniuse-lite");';
33-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
33+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
3434
"try {
3535
e.exports = require("caniuse-lite");
3636
} catch {
@@ -41,7 +41,7 @@ describe("optional dependecy", () => {
4141

4242
it('should wrap module.exports = require("caniuse-lite") in a try-catch', () => {
4343
const code = 'module.exports = require("caniuse-lite");';
44-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
44+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
4545
"try {
4646
module.exports = require("caniuse-lite");
4747
} catch {
@@ -52,7 +52,7 @@ describe("optional dependecy", () => {
5252

5353
it('should wrap exports.foo = require("caniuse-lite") in a try-catch', () => {
5454
const code = 'exports.foo = require("caniuse-lite");';
55-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
55+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
5656
"try {
5757
exports.foo = require("caniuse-lite");
5858
} catch {
@@ -63,23 +63,27 @@ describe("optional dependecy", () => {
6363

6464
it('should not wrap require("lodash") in a try-catch', () => {
6565
const code = 't = require("lodash");';
66-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("lodash");"`);
66+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(
67+
`"t = require("lodash");"`
68+
);
6769
});
6870

6971
it('should not wrap require("other-module") if it does not match caniuse-lite regex', () => {
7072
const code = 't = require("other-module");';
71-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("other-module");"`);
73+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(
74+
`"t = require("other-module");"`
75+
);
7276
});
7377

7478
it("should not wrap a require() call already inside a try-catch", () => {
7579
const code = `
7680
try {
77-
const t = require("caniuse-lite");
81+
t = require("caniuse-lite");
7882
} catch {}
7983
`;
80-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
84+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
8185
"try {
82-
const t = require("caniuse-lite");
86+
t = require("caniuse-lite");
8387
} catch {}
8488
"
8589
`);
@@ -88,14 +92,62 @@ try {
8892
it("should handle require with subpath and not wrap if already in try-catch", () => {
8993
const code = `
9094
try {
91-
const t = require("caniuse-lite/path");
95+
t = require("caniuse-lite/path");
9296
} catch {}
9397
`;
94-
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
98+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
9599
"try {
96-
const t = require("caniuse-lite/path");
100+
t = require("caniuse-lite/path");
97101
} catch {}
98102
"
99103
`);
100104
});
105+
106+
it("should handle multiple dependencies", () => {
107+
const code = `
108+
t1 = require("caniuse-lite");
109+
t2 = require("caniuse-lite/path");
110+
t3 = require("jimp");
111+
t4 = require("jimp/path");
112+
`;
113+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite", "jimp"]))).toMatchInlineSnapshot(`
114+
"try {
115+
t1 = require("caniuse-lite");
116+
} catch {
117+
throw new Error('The optional dependency "caniuse-lite" is not installed');
118+
};
119+
try {
120+
t2 = require("caniuse-lite/path");
121+
} catch {
122+
throw new Error('The optional dependency "caniuse-lite/path" is not installed');
123+
};
124+
try {
125+
t3 = require("jimp");
126+
} catch {
127+
throw new Error('The optional dependency "jimp" is not installed');
128+
};
129+
try {
130+
t4 = require("jimp/path");
131+
} catch {
132+
throw new Error('The optional dependency "jimp/path" is not installed');
133+
};
134+
"
135+
`);
136+
});
137+
138+
it("should not update partial matches", () => {
139+
const code = `
140+
t1 = require("before-caniuse-lite");
141+
t2 = require("before-caniuse-lite/path");
142+
t3 = require("caniuse-lite-after");
143+
t4 = require("caniuse-lite-after/path");
144+
`;
145+
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
146+
"t1 = require("before-caniuse-lite");
147+
t2 = require("before-caniuse-lite/path");
148+
t3 = require("caniuse-lite-after");
149+
t4 = require("caniuse-lite-after/path");
150+
"
151+
`);
152+
});
101153
});

packages/cloudflare/src/cli/build/patches/ast/optional-deps.ts

+33-20
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,40 @@ import { applyRule } from "./util.js";
99
*
1010
* So we wrap `require(optionalDep)` in a try/catch (if not already present).
1111
*/
12-
export const optionalDepRule = `
13-
rule:
14-
pattern: $$$LHS = require($$$REQ)
15-
has:
16-
pattern: $MOD
17-
kind: string_fragment
18-
stopBy: end
19-
regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)
20-
not:
21-
inside:
22-
kind: try_statement
12+
export function buildOptionalDepRule(dependencies: string[]) {
13+
// Build a regexp matching either
14+
// - the full packages names, i.e. `package`
15+
// - subpaths in the package, i.e. `package/...`
16+
const regex = `^(${dependencies.join("|")})(/|$)`;
17+
return `
18+
rule:
19+
pattern: $$$LHS = require($$$REQ)
20+
has:
21+
pattern: $MOD
22+
kind: string_fragment
2323
stopBy: end
24+
regex: ${regex}
25+
not:
26+
inside:
27+
kind: try_statement
28+
stopBy: end
2429
25-
fix: |-
26-
try {
27-
$$$LHS = require($$$REQ);
28-
} catch {
29-
throw new Error('The optional dependency "$MOD" is not installed');
30-
}
31-
`;
30+
fix: |-
31+
try {
32+
$$$LHS = require($$$REQ);
33+
} catch {
34+
throw new Error('The optional dependency "$MOD" is not installed');
35+
}
36+
`;
37+
}
3238

33-
export function patchOptionalDependencies(root: SgNode) {
34-
return applyRule(optionalDepRule, root);
39+
/**
40+
* Wraps requires for passed dependencies in a `try ... catch`.
41+
*
42+
* @param root AST root node
43+
* @param dependencies List of dependencies to wrap
44+
* @returns matches and edits, see `applyRule`
45+
*/
46+
export function patchOptionalDependencies(root: SgNode, dependencies: string[]) {
47+
return applyRule(buildOptionalDepRule(dependencies), root);
3548
}

0 commit comments

Comments
 (0)