@opennextjs/cloudflare 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,19 +18,16 @@ export function getCloudflareContext() {
18
18
  // the cloudflare context is initialized by the worker and is always present in production/preview
19
19
  // during local development (`next dev`) it might be missing only if the developers hasn't called
20
20
  // the `initOpenNextCloudflareForDev` function in their Next.js config file
21
- const getContextFunctionName = getCloudflareContext.name;
22
- const initFunctionName = initOpenNextCloudflareForDev.name;
23
- throw new Error(`\n\n\`${getContextFunctionName}\` has been called during development without having called` +
24
- ` the \`${initFunctionName}\` function inside the Next.js config file.\n\n` +
25
- `In order to use \`${getContextFunctionName}\` import and call ${initFunctionName} in the Next.js config file.\n\n` +
26
- "Example: \n ```\n // next.config.mjs\n\n" +
27
- ` import { ${initFunctionName} } from "@opennextjs/cloudflare";\n\n` +
28
- ` ${initFunctionName}();\n\n` +
29
- " /** @type {import('next').NextConfig} */\n" +
30
- " const nextConfig = {};\n" +
21
+ throw new Error(`\n\nERROR: \`getCloudflareContext\` has been called without having called` +
22
+ ` \`initOpenNextCloudflareForDev\` from the Next.js config file.\n` +
23
+ `You should update your Next.js config file as shown below:\n\n` +
24
+ " ```\n // next.config.mjs\n\n" +
25
+ ` import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";\n\n` +
26
+ ` initOpenNextCloudflareForDev();\n\n` +
27
+ " const nextConfig = { ... };\n" +
31
28
  " export default nextConfig;\n" +
32
29
  " ```\n" +
33
- "\n(note: currently middlewares in Next.js are always run using the edge runtime)\n\n");
30
+ "\n");
34
31
  }
35
32
  return cloudflareContext;
36
33
  }
@@ -3,6 +3,10 @@ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
3
3
  * Bundle the Open Next server.
4
4
  */
5
5
  export declare function bundleServer(buildOpts: BuildOptions): Promise<void>;
6
+ /**
7
+ * This function applies patches required for the code to run on workers.
8
+ */
9
+ export declare function updateWorkerBundledCode(workerOutputFile: string, buildOpts: BuildOptions): Promise<void>;
6
10
  /**
7
11
  * Gets the path of the worker.js file generated by the build process
8
12
  *
@@ -7,12 +7,27 @@ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
7
7
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
8
8
  import { build } from "esbuild";
9
9
  import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
10
+ import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
10
11
  import * as patches from "./patches/index.js";
12
+ import fixRequire from "./patches/plugins/require.js";
11
13
  import inlineRequirePagePlugin from "./patches/plugins/require-page.js";
12
14
  import setWranglerExternal from "./patches/plugins/wrangler-external.js";
13
15
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
14
16
  /** The dist directory of the Cloudflare adapter package */
15
17
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
18
+ /**
19
+ * List of optional Next.js dependencies.
20
+ * They are not required for Next.js to run but only needed to enabled specific features.
21
+ * When one of those dependency is required, it should be installed by the application.
22
+ */
23
+ const optionalDependencies = [
24
+ "caniuse-lite",
25
+ "critters",
26
+ "jimp",
27
+ "probe-image-size",
28
+ // `server.edge` is not available in react-dom@18
29
+ "react-dom/server.edge",
30
+ ];
16
31
  /**
17
32
  * Bundle the Open Next server.
18
33
  */
@@ -22,32 +37,27 @@ export async function bundleServer(buildOpts) {
22
37
  const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
23
38
  const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
24
39
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
25
- patches.patchWranglerDeps(buildOpts);
26
40
  await patches.updateWebpackChunksFile(buildOpts);
27
- patches.patchVercelOgLibrary(buildOpts);
41
+ patchVercelOgLibrary(buildOpts);
28
42
  const outputPath = path.join(outputDir, "server-functions", "default");
29
43
  const packagePath = getPackagePath(buildOpts);
30
44
  const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
31
45
  const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
32
- await build({
46
+ const result = await build({
33
47
  entryPoints: [openNextServer],
34
48
  bundle: true,
35
49
  outfile: openNextServerBundle,
36
50
  format: "esm",
37
51
  target: "esnext",
38
52
  minify: false,
53
+ metafile: true,
39
54
  plugins: [
40
55
  createFixRequiresESBuildPlugin(buildOpts),
41
56
  inlineRequirePagePlugin(buildOpts),
42
57
  setWranglerExternal(),
58
+ fixRequire(),
43
59
  ],
44
- external: [
45
- "./middleware/handler.mjs",
46
- // Next optional dependencies.
47
- "caniuse-lite",
48
- "jimp",
49
- "probe-image-size",
50
- ],
60
+ external: ["./middleware/handler.mjs", ...optionalDependencies],
51
61
  alias: {
52
62
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
53
63
  // eval("require")("bufferutil");
@@ -75,7 +85,6 @@ export async function bundleServer(buildOpts) {
75
85
  "process.env.NODE_ENV": '"production"',
76
86
  "process.env.NEXT_MINIMAL": "true",
77
87
  },
78
- // We need to set platform to node so that esbuild doesn't complain about the node imports
79
88
  platform: "node",
80
89
  banner: {
81
90
  js: `
@@ -103,7 +112,7 @@ const CustomRequest = class extends globalThis.Request {
103
112
  // https://github.com/cloudflare/workerd/issues/2746
104
113
  // https://github.com/cloudflare/workerd/issues/3245
105
114
  Object.defineProperty(init, "body", {
106
- value: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body;
115
+ value: init.body instanceof __cf_stream.Readable ? ReadableStream.from(init.body) : init.body
107
116
  });
108
117
  }
109
118
  super(input, init);
@@ -117,6 +126,11 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
117
126
  `,
118
127
  },
119
128
  });
129
+ if (result.errors.length > 0) {
130
+ result.errors.forEach((error) => console.error(error));
131
+ throw new Error(`There was a problem bundling the server.`);
132
+ }
133
+ fs.writeFileSync(openNextServerBundle + ".meta.json", JSON.stringify(result.metafile, null, 2));
120
134
  await updateWorkerBundledCode(openNextServerBundle, buildOpts);
121
135
  const isMonorepo = monorepoRoot !== appPath;
122
136
  if (isMonorepo) {
@@ -127,7 +141,7 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
127
141
  /**
128
142
  * This function applies patches required for the code to run on workers.
129
143
  */
130
- async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
144
+ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
131
145
  const code = await readFile(workerOutputFile, "utf8");
132
146
  const patchedCode = await patchCodeWithValidations(code, [
133
147
  ["require", patches.patchRequire],
@@ -148,11 +162,6 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
148
162
  // TODO: implement for cf (possibly in @opennextjs/aws)
149
163
  .replace("patchAsyncStorage();", "//patchAsyncStorage();"),
150
164
  ],
151
- [
152
- '`eval("require")` calls',
153
- (code) => code.replaceAll('eval("require")', "require"),
154
- { isOptional: true },
155
- ],
156
165
  [
157
166
  "`require.resolve` call",
158
167
  // workers do not support dynamic require nor require.resolve
@@ -160,7 +169,7 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
160
169
  ],
161
170
  ]);
162
171
  const bundle = parse(Lang.TypeScript, patchedCode).root();
163
- const { edits } = patchOptionalDependencies(bundle);
172
+ const { edits } = patchOptionalDependencies(bundle, optionalDependencies);
164
173
  await writeFile(workerOutputFile, bundle.commitEdits(edits));
165
174
  }
166
175
  function createFixRequiresESBuildPlugin(options) {
@@ -6,8 +6,15 @@ import { type SgNode } from "@ast-grep/napi";
6
6
  *
7
7
  * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
8
  */
9
- export declare const optionalDepRule = "\nrule:\n pattern: $$$LHS = require($$$REQ)\n has:\n pattern: $MOD\n kind: string_fragment\n stopBy: end\n regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)\n not:\n inside:\n kind: try_statement\n stopBy: end\n\nfix: |-\n try {\n $$$LHS = require($$$REQ);\n } catch {\n throw new Error('The optional dependency \"$MOD\" is not installed');\n }\n";
10
- export declare function patchOptionalDependencies(root: SgNode): {
9
+ export declare function buildOptionalDepRule(dependencies: string[]): string;
10
+ /**
11
+ * Wraps requires for passed dependencies in a `try ... catch`.
12
+ *
13
+ * @param root AST root node
14
+ * @param dependencies List of dependencies to wrap
15
+ * @returns matches and edits, see `applyRule`
16
+ */
17
+ export declare function patchOptionalDependencies(root: SgNode, dependencies: string[]): {
11
18
  edits: import("@ast-grep/napi").Edit[];
12
19
  matches: SgNode<import("@ast-grep/napi/types/staticTypes.js").TypesMap, import("@ast-grep/napi/types/staticTypes.js").Kinds<import("@ast-grep/napi/types/staticTypes.js").TypesMap>>[];
13
20
  };
@@ -6,26 +6,39 @@ import { applyRule } from "./util.js";
6
6
  *
7
7
  * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
8
  */
9
- export const optionalDepRule = `
10
- rule:
11
- pattern: $$$LHS = require($$$REQ)
12
- has:
13
- pattern: $MOD
14
- kind: string_fragment
15
- stopBy: end
16
- regex: ^(caniuse-lite|jimp|probe-image-size)(/|$)
17
- not:
18
- inside:
19
- kind: try_statement
9
+ export function buildOptionalDepRule(dependencies) {
10
+ // Build a regexp matching either
11
+ // - the full packages names, i.e. `package`
12
+ // - subpaths in the package, i.e. `package/...`
13
+ const regex = `^(${dependencies.join("|")})(/|$)`;
14
+ return `
15
+ rule:
16
+ pattern: $$$LHS = require($$$REQ)
17
+ has:
18
+ pattern: $MOD
19
+ kind: string_fragment
20
20
  stopBy: end
21
+ regex: ${regex}
22
+ not:
23
+ inside:
24
+ kind: try_statement
25
+ stopBy: end
21
26
 
22
- fix: |-
23
- try {
24
- $$$LHS = require($$$REQ);
25
- } catch {
26
- throw new Error('The optional dependency "$MOD" is not installed');
27
- }
28
- `;
29
- export function patchOptionalDependencies(root) {
30
- return applyRule(optionalDepRule, root);
27
+ fix: |-
28
+ try {
29
+ $$$LHS = require($$$REQ);
30
+ } catch {
31
+ throw new Error('The optional dependency "$MOD" is not installed');
32
+ }
33
+ `;
34
+ }
35
+ /**
36
+ * Wraps requires for passed dependencies in a `try ... catch`.
37
+ *
38
+ * @param root AST root node
39
+ * @param dependencies List of dependencies to wrap
40
+ * @returns matches and edits, see `applyRule`
41
+ */
42
+ export function patchOptionalDependencies(root, dependencies) {
43
+ return applyRule(buildOptionalDepRule(dependencies), root);
31
44
  }
@@ -1,10 +1,10 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { optionalDepRule } from "./optional-deps.js";
2
+ import { buildOptionalDepRule } from "./optional-deps.js";
3
3
  import { patchCode } from "./util.js";
4
4
  describe("optional dependecy", () => {
5
5
  it('should wrap a top-level require("caniuse-lite") in a try-catch', () => {
6
6
  const code = `t = require("caniuse-lite");`;
7
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
7
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
8
8
  "try {
9
9
  t = require("caniuse-lite");
10
10
  } catch {
@@ -14,7 +14,7 @@ describe("optional dependecy", () => {
14
14
  });
15
15
  it('should wrap a top-level require("caniuse-lite/data") in a try-catch', () => {
16
16
  const code = `t = require("caniuse-lite/data");`;
17
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
17
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
18
18
  "try {
19
19
  t = require("caniuse-lite/data");
20
20
  } catch {
@@ -24,7 +24,7 @@ describe("optional dependecy", () => {
24
24
  });
25
25
  it('should wrap e.exports = require("caniuse-lite") in a try-catch', () => {
26
26
  const code = 'e.exports = require("caniuse-lite");';
27
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
27
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
28
28
  "try {
29
29
  e.exports = require("caniuse-lite");
30
30
  } catch {
@@ -34,7 +34,7 @@ describe("optional dependecy", () => {
34
34
  });
35
35
  it('should wrap module.exports = require("caniuse-lite") in a try-catch', () => {
36
36
  const code = 'module.exports = require("caniuse-lite");';
37
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
37
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
38
38
  "try {
39
39
  module.exports = require("caniuse-lite");
40
40
  } catch {
@@ -44,7 +44,7 @@ describe("optional dependecy", () => {
44
44
  });
45
45
  it('should wrap exports.foo = require("caniuse-lite") in a try-catch', () => {
46
46
  const code = 'exports.foo = require("caniuse-lite");';
47
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
47
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
48
48
  "try {
49
49
  exports.foo = require("caniuse-lite");
50
50
  } catch {
@@ -54,21 +54,21 @@ describe("optional dependecy", () => {
54
54
  });
55
55
  it('should not wrap require("lodash") in a try-catch', () => {
56
56
  const code = 't = require("lodash");';
57
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("lodash");"`);
57
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("lodash");"`);
58
58
  });
59
59
  it('should not wrap require("other-module") if it does not match caniuse-lite regex', () => {
60
60
  const code = 't = require("other-module");';
61
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`"t = require("other-module");"`);
61
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("other-module");"`);
62
62
  });
63
63
  it("should not wrap a require() call already inside a try-catch", () => {
64
64
  const code = `
65
65
  try {
66
- const t = require("caniuse-lite");
66
+ t = require("caniuse-lite");
67
67
  } catch {}
68
68
  `;
69
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
69
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
70
70
  "try {
71
- const t = require("caniuse-lite");
71
+ t = require("caniuse-lite");
72
72
  } catch {}
73
73
  "
74
74
  `);
@@ -76,14 +76,60 @@ try {
76
76
  it("should handle require with subpath and not wrap if already in try-catch", () => {
77
77
  const code = `
78
78
  try {
79
- const t = require("caniuse-lite/path");
79
+ t = require("caniuse-lite/path");
80
80
  } catch {}
81
81
  `;
82
- expect(patchCode(code, optionalDepRule)).toMatchInlineSnapshot(`
82
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
83
83
  "try {
84
- const t = require("caniuse-lite/path");
84
+ t = require("caniuse-lite/path");
85
85
  } catch {}
86
86
  "
87
87
  `);
88
88
  });
89
+ it("should handle multiple dependencies", () => {
90
+ const code = `
91
+ t1 = require("caniuse-lite");
92
+ t2 = require("caniuse-lite/path");
93
+ t3 = require("jimp");
94
+ t4 = require("jimp/path");
95
+ `;
96
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite", "jimp"]))).toMatchInlineSnapshot(`
97
+ "try {
98
+ t1 = require("caniuse-lite");
99
+ } catch {
100
+ throw new Error('The optional dependency "caniuse-lite" is not installed');
101
+ };
102
+ try {
103
+ t2 = require("caniuse-lite/path");
104
+ } catch {
105
+ throw new Error('The optional dependency "caniuse-lite/path" is not installed');
106
+ };
107
+ try {
108
+ t3 = require("jimp");
109
+ } catch {
110
+ throw new Error('The optional dependency "jimp" is not installed');
111
+ };
112
+ try {
113
+ t4 = require("jimp/path");
114
+ } catch {
115
+ throw new Error('The optional dependency "jimp/path" is not installed');
116
+ };
117
+ "
118
+ `);
119
+ });
120
+ it("should not update partial matches", () => {
121
+ const code = `
122
+ t1 = require("before-caniuse-lite");
123
+ t2 = require("before-caniuse-lite/path");
124
+ t3 = require("caniuse-lite-after");
125
+ t4 = require("caniuse-lite-after/path");
126
+ `;
127
+ expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
128
+ "t1 = require("before-caniuse-lite");
129
+ t2 = require("before-caniuse-lite/path");
130
+ t3 = require("caniuse-lite-after");
131
+ t4 = require("caniuse-lite-after/path");
132
+ "
133
+ `);
134
+ });
89
135
  });
@@ -2,8 +2,8 @@ import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } fro
2
2
  import path from "node:path";
3
3
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
4
  import { globSync } from "glob";
5
- import { parseFile } from "../ast/util.js";
6
- import { patchVercelOgFallbackFont, patchVercelOgImport } from "../ast/vercel-og.js";
5
+ import { parseFile } from "./util.js";
6
+ import { patchVercelOgFallbackFont, patchVercelOgImport } from "./vercel-og.js";
7
7
  /**
8
8
  * Patches the usage of @vercel/og to be compatible with Cloudflare Workers.
9
9
  *
@@ -1,5 +1,4 @@
1
1
  export * from "./copy-package-cli-files.js";
2
2
  export * from "./patch-cache.js";
3
3
  export * from "./patch-require.js";
4
- export * from "./patch-vercel-og-library.js";
5
4
  export * from "./update-webpack-chunks-file/index.js";
@@ -1,5 +1,4 @@
1
1
  export * from "./copy-package-cli-files.js";
2
2
  export * from "./patch-cache.js";
3
3
  export * from "./patch-require.js";
4
- export * from "./patch-vercel-og-library.js";
5
4
  export * from "./update-webpack-chunks-file/index.js";
@@ -0,0 +1,5 @@
1
+ import type { PluginBuild } from "esbuild";
2
+ export default function fixRequire(): {
3
+ name: string;
4
+ setup: (build: PluginBuild) => Promise<void>;
5
+ };
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs/promises";
2
+ export default function fixRequire() {
3
+ return {
4
+ name: "fix-require",
5
+ setup: async (build) => {
6
+ build.onLoad({ filter: /.*/ }, async ({ path }) => {
7
+ let contents = await fs.readFile(path, "utf-8");
8
+ // `eval(...)` is not supported by workerd.
9
+ contents = contents.replaceAll(`eval("require")`, "require");
10
+ // `@opentelemetry` has a few issues.
11
+ //
12
+ // Next.js has the following code in `next/dist/server/lib/trace/tracer.js`:
13
+ //
14
+ // try {
15
+ // api = require('@opentelemetry/api');
16
+ // } catch (err) {
17
+ // api = require('next/dist/compiled/@opentelemetry/api');
18
+ // }
19
+ //
20
+ // The intent is to allow users to install their own version of `@opentelemetry/api`.
21
+ //
22
+ // The problem is that even when users do not explicitely install `@opentelemetry/api`,
23
+ // `require('@opentelemetry/api')` resolves to the package which is a dependency
24
+ // of Next.
25
+ //
26
+ // The second problem is that when Next traces files, it would not copy the `api/build/esm`
27
+ // folder (used by the `module` conditions in package.json) it would only copy `api/build/src`.
28
+ // This could be solved by updating the next config:
29
+ //
30
+ // const nextConfig: NextConfig = {
31
+ // // ...
32
+ // outputFileTracingIncludes: {
33
+ // "*": ["./node_modules/@opentelemetry/api/build/**/*"],
34
+ // },
35
+ // };
36
+ //
37
+ // We can consider doing that when we want to enable users to install their own version
38
+ // of `@opentelemetry/api`. For now we simply use the pre-compiled version.
39
+ contents = contents.replace(/require\(.@opentelemetry\/api.\)/g, `require("next/dist/compiled/@opentelemetry/api")`);
40
+ return { contents };
41
+ });
42
+ },
43
+ };
44
+ }
@@ -4,4 +4,3 @@ export * from "./patch-exception-bubbling.js";
4
4
  export * from "./patch-find-dir.js";
5
5
  export * from "./patch-load-instrumentation-module.js";
6
6
  export * from "./patch-read-file.js";
7
- export * from "./wrangler-deps.js";
@@ -4,4 +4,3 @@ export * from "./patch-exception-bubbling.js";
4
4
  export * from "./patch-find-dir.js";
5
5
  export * from "./patch-load-instrumentation-module.js";
6
6
  export * from "./patch-read-file.js";
7
- export * from "./wrangler-deps.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@opennextjs/cloudflare",
3
3
  "description": "Cloudflare builder for next apps",
4
- "version": "0.4.1",
4
+ "version": "0.4.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -47,7 +47,7 @@
47
47
  "@tsconfig/strictest": "^2.0.5",
48
48
  "@types/mock-fs": "^4.13.4",
49
49
  "@types/node": "^22.2.0",
50
- "esbuild": "^0.23.0",
50
+ "esbuild": "^0.24.2",
51
51
  "eslint": "^9.11.1",
52
52
  "eslint-plugin-import": "^2.31.0",
53
53
  "eslint-plugin-simple-import-sort": "^12.1.1",
@@ -63,14 +63,14 @@
63
63
  "dependencies": {
64
64
  "@ast-grep/napi": "^0.34.1",
65
65
  "@dotenvx/dotenvx": "1.31.0",
66
- "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@712",
66
+ "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@715",
67
67
  "enquirer": "^2.4.1",
68
68
  "glob": "^11.0.0",
69
69
  "ts-morph": "^23.0.0",
70
70
  "yaml": "^2.7.0"
71
71
  },
72
72
  "peerDependencies": {
73
- "wrangler": "^3.105.0"
73
+ "wrangler": "^3.107.0"
74
74
  },
75
75
  "scripts": {
76
76
  "clean": "rimraf dist",
@@ -1,2 +0,0 @@
1
- import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
- export declare function patchWranglerDeps(buildOpts: BuildOptions): void;
@@ -1,115 +0,0 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
- import * as ts from "ts-morph";
5
- import { tsParseFile } from "../../utils/index.js";
6
- export function patchWranglerDeps(buildOpts) {
7
- console.log("# patchWranglerDeps");
8
- const { outputDir } = buildOpts;
9
- const nextDistDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), "node_modules/next/dist");
10
- const pagesRuntimeFile = join(nextDistDir, "compiled/next-server/pages.runtime.prod.js");
11
- const patchedPagesRuntime = readFileSync(pagesRuntimeFile, "utf-8").replace(`e.exports=require("critters")`, `
12
- try {
13
- e.exports=require("critters");
14
- }
15
- catch {
16
- console.error('critters is not installed');
17
- }
18
- `);
19
- writeFileSync(pagesRuntimeFile, patchedPagesRuntime);
20
- patchRequireReactDomServerEdge(nextDistDir);
21
- // we shim @opentelemetry/api to the throwing shim so that it will throw right away, this is so that we throw inside the
22
- // try block here: https://github.com/vercel/next.js/blob/9e8266a7/packages/next/src/server/lib/trace/tracer.ts#L27-L31
23
- // causing the code to require the 'next/dist/compiled/@opentelemetry/api' module instead (which properly works)
24
- const tracerFile = join(nextDistDir, "server/lib/trace/tracer.js");
25
- const patchedTracer = readFileSync(tracerFile, "utf-8").replaceAll(/\w+\s*=\s*require\([^/]*opentelemetry.*\)/g, `throw new Error("@opentelemetry/api")`);
26
- writeFileSync(tracerFile, patchedTracer);
27
- }
28
- /**
29
- * `react-dom` v>=19 has a `server.edge` export: https://github.com/facebook/react/blob/a160102f3/packages/react-dom/package.json#L79
30
- * but version of `react-dom` <= 18 do not have this export but have a `server.browser` export instead: https://github.com/facebook/react/blob/8a015b68/packages/react-dom/package.json#L49
31
- *
32
- * Next.js also try-catches importing the `server.edge` export:
33
- * https://github.com/vercel/next.js/blob/6784575/packages/next/src/server/ReactDOMServerPages.js
34
- *
35
- * The issue here is that in the `.next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js`
36
- * file for whatever reason there is a non `try-catch`ed require for the `server.edge` export
37
- *
38
- * This functions fixes this issue by wrapping the require in a try-catch block in the same way Next.js does it
39
- * (note: this will make the build succeed but doesn't guarantee that everything will necessarily work at runtime since
40
- * it's not clear what code and how might be rely on this require call)
41
- *
42
- */
43
- function patchRequireReactDomServerEdge(nextDistDir) {
44
- // Patch .next/standalone/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js
45
- const pagesRuntimeFile = join(nextDistDir, "compiled/next-server/pages.runtime.prod.js");
46
- const code = readFileSync(pagesRuntimeFile, "utf-8");
47
- const file = tsParseFile(code);
48
- // we need to update this function: `e=>{"use strict";e.exports=require("react-dom/server.edge")}`
49
- file.getDescendantsOfKind(ts.SyntaxKind.ArrowFunction).forEach((arrowFunction) => {
50
- // the function has a single parameter
51
- const p = arrowFunction.getParameters();
52
- if (p.length !== 1) {
53
- return;
54
- }
55
- const parameterName = p[0].getName();
56
- const bodyChildren = arrowFunction.getBody().getChildren();
57
- if (!(bodyChildren.length === 3 &&
58
- bodyChildren[0].getFullText() === "{" &&
59
- bodyChildren[2].getFullText() === "}")) {
60
- return;
61
- }
62
- const bodyStatements = bodyChildren[1]?.getChildren();
63
- // the function has only two statements: "use strict" and e.exports=require("react-dom/server.edge")
64
- if (!(bodyStatements?.length === 2 &&
65
- bodyStatements.every((statement) => statement.isKind(ts.SyntaxKind.ExpressionStatement)))) {
66
- return;
67
- }
68
- const bodyExpressionStatements = bodyStatements;
69
- const stringLiteralExpression = bodyExpressionStatements[0].getExpressionIfKind(ts.SyntaxKind.StringLiteral);
70
- // the first statement needs to be "use strict"
71
- if (stringLiteralExpression?.getText() !== '"use strict"') {
72
- return;
73
- }
74
- // the second statement (e.exports=require("react-dom/server.edge")) needs to be a binary expression
75
- const binaryExpression = bodyExpressionStatements[1].getExpressionIfKind(ts.SyntaxKind.BinaryExpression);
76
- if (!binaryExpression?.getOperatorToken().isKind(ts.SyntaxKind.EqualsToken)) {
77
- return;
78
- }
79
- // on the left we have `${parameterName}.exports`
80
- const binaryLeft = binaryExpression.getLeft();
81
- if (!binaryLeft.isKind(ts.SyntaxKind.PropertyAccessExpression) ||
82
- binaryLeft.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== parameterName ||
83
- binaryLeft.getName() !== "exports") {
84
- return;
85
- }
86
- // on the right we have `require("react-dom/server.edge")`
87
- const binaryRight = binaryExpression.getRight();
88
- if (!binaryRight.isKind(ts.SyntaxKind.CallExpression) ||
89
- binaryRight.getExpressionIfKind(ts.SyntaxKind.Identifier)?.getText() !== "require") {
90
- return;
91
- }
92
- const requireArgs = binaryRight.getArguments();
93
- if (requireArgs.length !== 1 || requireArgs[0].getText() !== '"react-dom/server.edge"') {
94
- return;
95
- }
96
- arrowFunction.setBodyText(`
97
- // OpenNext patch
98
- let ReactDOMServer;
99
- try {
100
- ReactDOMServer = require('react-dom/server.edge');
101
- } catch (error) {
102
- if (
103
- error.code !== 'MODULE_NOT_FOUND' &&
104
- error.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED'
105
- ) {
106
- throw error;
107
- }
108
- ReactDOMServer = require('react-dom/server.browser');
109
- }
110
- ${parameterName}.exports = ReactDOMServer;
111
- `);
112
- });
113
- const updatedCode = file.print();
114
- writeFileSync(pagesRuntimeFile, updatedCode);
115
- }