Skip to content

Commit

Permalink
refactor: Make the list of optional dependencies configurable (#297)
Browse files Browse the repository at this point in the history
  • Loading branch information
vicb authored Jan 29, 2025
1 parent 1b3a972 commit 5c90521
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 176 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-walls-hug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/cloudflare": patch
---

refactor: Make the list of optional dependencies configurable
24 changes: 16 additions & 8 deletions packages/cloudflare/src/cli/build/bundle-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
/** The dist directory of the Cloudflare adapter package */
const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");

/**
* List of optional Next.js dependencies.
* They are not required for Next.js to run but only needed to enabled specific features.
* When one of those dependency is required, it should be installed by the application.
*/
const optionalDependencies = [
"caniuse-lite",
"critters",
"jimp",
"probe-image-size",
// `server.edge` is not available in react-dom@18
"react-dom/server.edge",
];

/**
* Bundle the Open Next server.
*/
Expand Down Expand Up @@ -56,13 +70,7 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
inlineRequirePagePlugin(buildOpts),
setWranglerExternal(),
],
external: [
"./middleware/handler.mjs",
// Next optional dependencies.
"caniuse-lite",
"jimp",
"probe-image-size",
],
external: ["./middleware/handler.mjs", ...optionalDependencies],
alias: {
// Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
// eval("require")("bufferutil");
Expand Down Expand Up @@ -196,7 +204,7 @@ async function updateWorkerBundledCode(workerOutputFile: string, buildOpts: Buil

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

const { edits } = patchOptionalDependencies(bundle);
const { edits } = patchOptionalDependencies(bundle, optionalDependencies);

await writeFile(workerOutputFile, bundle.commitEdits(edits));
}
Expand Down
80 changes: 66 additions & 14 deletions packages/cloudflare/src/cli/build/patches/ast/optional-deps.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { describe, expect, it } from "vitest";

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

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

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

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

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

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

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

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

it("should not wrap a require() call already inside a try-catch", () => {
const code = `
try {
const t = require("caniuse-lite");
t = require("caniuse-lite");
} catch {}
`;
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
"try {
const t = require("caniuse-lite");
t = require("caniuse-lite");
} catch {}
"
`);
Expand All @@ -88,14 +92,62 @@ try {
it("should handle require with subpath and not wrap if already in try-catch", () => {
const code = `
try {
const t = require("caniuse-lite/path");
t = require("caniuse-lite/path");
} catch {}
`;
expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
"try {
const t = require("caniuse-lite/path");
t = require("caniuse-lite/path");
} catch {}
"
`);
});

it("should handle multiple dependencies", () => {
const code = `
t1 = require("caniuse-lite");
t2 = require("caniuse-lite/path");
t3 = require("jimp");
t4 = require("jimp/path");
`;
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite", "jimp"]))).toMatchInlineSnapshot(`
"try {
t1 = require("caniuse-lite");
} catch {
throw new Error('The optional dependency "caniuse-lite" is not installed');
};
try {
t2 = require("caniuse-lite/path");
} catch {
throw new Error('The optional dependency "caniuse-lite/path" is not installed');
};
try {
t3 = require("jimp");
} catch {
throw new Error('The optional dependency "jimp" is not installed');
};
try {
t4 = require("jimp/path");
} catch {
throw new Error('The optional dependency "jimp/path" is not installed');
};
"
`);
});

it("should not update partial matches", () => {
const code = `
t1 = require("before-caniuse-lite");
t2 = require("before-caniuse-lite/path");
t3 = require("caniuse-lite-after");
t4 = require("caniuse-lite-after/path");
`;
expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
"t1 = require("before-caniuse-lite");
t2 = require("before-caniuse-lite/path");
t3 = require("caniuse-lite-after");
t4 = require("caniuse-lite-after/path");
"
`);
});
});
53 changes: 33 additions & 20 deletions packages/cloudflare/src/cli/build/patches/ast/optional-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,40 @@ import { applyRule } from "./util.js";
*
* So we wrap `require(optionalDep)` in a try/catch (if not already present).
*/
export const optionalDepRule = `
rule:
pattern: $$$LHS = require($$$REQ)
has:
pattern: $MOD
kind: string_fragment
stopBy: end
regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)
not:
inside:
kind: try_statement
export function buildOptionalDepRule(dependencies: string[]) {
// Build a regexp matching either
// - the full packages names, i.e. `package`
// - subpaths in the package, i.e. `package/...`
const regex = `^(${dependencies.join("|")})(/|$)`;
return `
rule:
pattern: $$$LHS = require($$$REQ)
has:
pattern: $MOD
kind: string_fragment
stopBy: end
regex: ${regex}
not:
inside:
kind: try_statement
stopBy: end
fix: |-
try {
$$$LHS = require($$$REQ);
} catch {
throw new Error('The optional dependency "$MOD" is not installed');
}
`;
fix: |-
try {
$$$LHS = require($$$REQ);
} catch {
throw new Error('The optional dependency "$MOD" is not installed');
}
`;
}

export function patchOptionalDependencies(root: SgNode) {
return applyRule(optionalDepRule, root);
/**
* Wraps requires for passed dependencies in a `try ... catch`.
*
* @param root AST root node
* @param dependencies List of dependencies to wrap
* @returns matches and edits, see `applyRule`
*/
export function patchOptionalDependencies(root: SgNode, dependencies: string[]) {
return applyRule(buildOptionalDepRule(dependencies), root);
}
Loading

0 comments on commit 5c90521

Please sign in to comment.