Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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