@opennextjs/cloudflare 0.4.0 → 0.4.2

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.
Files changed (25) hide show
  1. package/dist/api/cloudflare-context.js +18 -0
  2. package/dist/cli/build/bundle-server.d.ts +4 -0
  3. package/dist/cli/build/bundle-server.js +35 -15
  4. package/dist/cli/build/patches/ast/optional-deps.d.ts +10 -3
  5. package/dist/cli/build/patches/ast/optional-deps.js +34 -21
  6. package/dist/cli/build/patches/ast/optional-deps.spec.js +60 -14
  7. package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.js +2 -2
  8. package/dist/cli/build/patches/investigated/index.d.ts +0 -1
  9. package/dist/cli/build/patches/investigated/index.js +0 -1
  10. package/dist/cli/build/patches/plugins/require-page.d.ts +6 -0
  11. package/dist/cli/build/patches/plugins/require-page.js +70 -0
  12. package/dist/cli/build/patches/plugins/require.d.ts +5 -0
  13. package/dist/cli/build/patches/plugins/require.js +44 -0
  14. package/dist/cli/build/patches/plugins/wrangler-external.d.ts +20 -0
  15. package/dist/cli/build/patches/plugins/wrangler-external.js +36 -0
  16. package/dist/cli/build/patches/to-investigate/index.d.ts +0 -2
  17. package/dist/cli/build/patches/to-investigate/index.js +0 -2
  18. package/package.json +4 -4
  19. package/dist/cli/build/patches/to-investigate/inline-next-require.d.ts +0 -6
  20. package/dist/cli/build/patches/to-investigate/inline-next-require.js +0 -40
  21. package/dist/cli/build/patches/to-investigate/wrangler-deps.d.ts +0 -2
  22. package/dist/cli/build/patches/to-investigate/wrangler-deps.js +0 -115
  23. /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.d.ts +0 -0
  24. /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.spec.d.ts +0 -0
  25. /package/dist/cli/build/patches/{investigated → ast}/patch-vercel-og-library.spec.js +0 -0
@@ -41,10 +41,28 @@ export function getCloudflareContext() {
41
41
  * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
42
42
  */
43
43
  export async function initOpenNextCloudflareForDev() {
44
+ const shouldInitializationRun = shouldContextInitializationRun();
45
+ if (!shouldInitializationRun)
46
+ return;
44
47
  const context = await getCloudflareContextFromWrangler();
45
48
  addCloudflareContextToNodejsGlobal(context);
46
49
  await monkeyPatchVmModuleEdgeContext(context);
47
50
  }
51
+ /**
52
+ * Next dev server imports the config file twice (in two different processes, making it hard to track),
53
+ * this causes the initialization to run twice as well, to keep things clean, not allocate extra
54
+ * resources (i.e. instantiate two miniflare instances) and avoid extra potential logs, it would be best
55
+ * to run the initialization only once, this function is used to try to make it so that it does, it returns
56
+ * a flag which indicates if the initialization should run in the current process or not.
57
+ *
58
+ * @returns boolean indicating if the initialization should run
59
+ */
60
+ function shouldContextInitializationRun() {
61
+ // via debugging we've seen that AsyncLocalStorage is only set in one of the
62
+ // two processes so we're using it as the differentiator between the two
63
+ const AsyncLocalStorage = globalThis["AsyncLocalStorage"];
64
+ return !!AsyncLocalStorage;
65
+ }
48
66
  /**
49
67
  * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
50
68
  * future calls to `getCloudflareContext` to retrieve and return such context
@@ -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,10 +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";
13
+ import inlineRequirePagePlugin from "./patches/plugins/require-page.js";
14
+ import setWranglerExternal from "./patches/plugins/wrangler-external.js";
11
15
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
12
16
  /** The dist directory of the Cloudflare adapter package */
13
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
+ ];
14
31
  /**
15
32
  * Bundle the Open Next server.
16
33
  */
@@ -20,22 +37,27 @@ export async function bundleServer(buildOpts) {
20
37
  const serverFiles = path.join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/required-server-files.json");
21
38
  const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
22
39
  console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
23
- patches.patchWranglerDeps(buildOpts);
24
40
  await patches.updateWebpackChunksFile(buildOpts);
25
- patches.patchVercelOgLibrary(buildOpts);
41
+ patchVercelOgLibrary(buildOpts);
26
42
  const outputPath = path.join(outputDir, "server-functions", "default");
27
43
  const packagePath = getPackagePath(buildOpts);
28
44
  const openNextServer = path.join(outputPath, packagePath, `index.mjs`);
29
45
  const openNextServerBundle = path.join(outputPath, packagePath, `handler.mjs`);
30
- await build({
46
+ const result = await build({
31
47
  entryPoints: [openNextServer],
32
48
  bundle: true,
33
49
  outfile: openNextServerBundle,
34
50
  format: "esm",
35
51
  target: "esnext",
36
52
  minify: false,
37
- plugins: [createFixRequiresESBuildPlugin(buildOpts)],
38
- external: ["./middleware/handler.mjs", "caniuse-lite"],
53
+ metafile: true,
54
+ plugins: [
55
+ createFixRequiresESBuildPlugin(buildOpts),
56
+ inlineRequirePagePlugin(buildOpts),
57
+ setWranglerExternal(),
58
+ fixRequire(),
59
+ ],
60
+ external: ["./middleware/handler.mjs", ...optionalDependencies],
39
61
  alias: {
40
62
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
41
63
  // eval("require")("bufferutil");
@@ -63,7 +85,6 @@ export async function bundleServer(buildOpts) {
63
85
  "process.env.NODE_ENV": '"production"',
64
86
  "process.env.NEXT_MINIMAL": "true",
65
87
  },
66
- // We need to set platform to node so that esbuild doesn't complain about the node imports
67
88
  platform: "node",
68
89
  banner: {
69
90
  js: `
@@ -91,7 +112,7 @@ const CustomRequest = class extends globalThis.Request {
91
112
  // https://github.com/cloudflare/workerd/issues/2746
92
113
  // https://github.com/cloudflare/workerd/issues/3245
93
114
  Object.defineProperty(init, "body", {
94
- 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
95
116
  });
96
117
  }
97
118
  super(input, init);
@@ -105,6 +126,11 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
105
126
  `,
106
127
  },
107
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));
108
134
  await updateWorkerBundledCode(openNextServerBundle, buildOpts);
109
135
  const isMonorepo = monorepoRoot !== appPath;
110
136
  if (isMonorepo) {
@@ -115,13 +141,12 @@ globalThis.__BUILD_TIMESTAMP_MS__ = ${Date.now()};
115
141
  /**
116
142
  * This function applies patches required for the code to run on workers.
117
143
  */
118
- async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
144
+ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
119
145
  const code = await readFile(workerOutputFile, "utf8");
120
146
  const patchedCode = await patchCodeWithValidations(code, [
121
147
  ["require", patches.patchRequire],
122
148
  ["`buildId` function", (code) => patches.patchBuildId(code, buildOpts)],
123
149
  ["`loadManifest` function", (code) => patches.patchLoadManifest(code, buildOpts)],
124
- ["next's require", (code) => patches.inlineNextRequire(code, buildOpts)],
125
150
  ["`findDir` function", (code) => patches.patchFindDir(code, buildOpts)],
126
151
  ["`evalManifest` function", (code) => patches.inlineEvalManifest(code, buildOpts)],
127
152
  ["cacheHandler", (code) => patches.patchCache(code, buildOpts)],
@@ -137,11 +162,6 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
137
162
  // TODO: implement for cf (possibly in @opennextjs/aws)
138
163
  .replace("patchAsyncStorage();", "//patchAsyncStorage();"),
139
164
  ],
140
- [
141
- '`eval("require")` calls',
142
- (code) => code.replaceAll('eval("require")', "require"),
143
- { isOptional: true },
144
- ],
145
165
  [
146
166
  "`require.resolve` call",
147
167
  // workers do not support dynamic require nor require.resolve
@@ -149,7 +169,7 @@ async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
149
169
  ],
150
170
  ]);
151
171
  const bundle = parse(Lang.TypeScript, patchedCode).root();
152
- const { edits } = patchOptionalDependencies(bundle);
172
+ const { edits } = patchOptionalDependencies(bundle, optionalDependencies);
153
173
  await writeFile(workerOutputFile, bundle.commitEdits(edits));
154
174
  }
155
175
  function createFixRequiresESBuildPlugin(options) {
@@ -1,13 +1,20 @@
1
1
  import { type SgNode } from "@ast-grep/napi";
2
2
  /**
3
- * Handle optional dependencies.
3
+ * Handles optional dependencies.
4
4
  *
5
5
  * A top level `require(optionalDep)` would throw when the dep is not installed.
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(/|$)\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
  };
@@ -1,31 +1,44 @@
1
1
  import { applyRule } from "./util.js";
2
2
  /**
3
- * Handle optional dependencies.
3
+ * Handles optional dependencies.
4
4
  *
5
5
  * A top level `require(optionalDep)` would throw when the dep is not installed.
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(/|$)
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,6 @@
1
+ import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
+ import type { PluginBuild } from "esbuild";
3
+ export default function inlineRequirePagePlugin(buildOpts: BuildOptions): {
4
+ name: string;
5
+ setup: (build: PluginBuild) => Promise<void>;
6
+ };
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { getPackagePath } from "@opennextjs/aws/build/helper.js";
5
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6
+ import { patchCode } from "../ast/util.js";
7
+ export default function inlineRequirePagePlugin(buildOpts) {
8
+ return {
9
+ name: "inline-require-page",
10
+ setup: async (build) => {
11
+ build.onLoad({
12
+ filter: getCrossPlatformPathRegex(String.raw `/next/dist/server/require\.js$`, { escape: false }),
13
+ }, async ({ path }) => {
14
+ const jsCode = await readFile(path, "utf8");
15
+ if (/function requirePage\(/.test(jsCode)) {
16
+ return { contents: patchCode(jsCode, getRule(buildOpts)) };
17
+ }
18
+ });
19
+ },
20
+ };
21
+ }
22
+ function getRule(buildOpts) {
23
+ const { outputDir } = buildOpts;
24
+ const serverDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
25
+ const pagesManifestFile = join(serverDir, "pages-manifest.json");
26
+ const appPathsManifestFile = join(serverDir, "app-paths-manifest.json");
27
+ const pagesManifests = existsSync(pagesManifestFile)
28
+ ? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8")))
29
+ : [];
30
+ const appPathsManifests = existsSync(appPathsManifestFile)
31
+ ? Object.values(JSON.parse(readFileSync(appPathsManifestFile, "utf-8")))
32
+ : [];
33
+ const manifests = pagesManifests.concat(appPathsManifests);
34
+ const htmlFiles = manifests.filter((file) => file.endsWith(".html"));
35
+ const jsFiles = manifests.filter((file) => file.endsWith(".js"));
36
+ // Inline fs access and dynamic require that are not supported by workerd.
37
+ const fnBody = `
38
+ // html
39
+ ${htmlFiles
40
+ .map((file) => `if (pagePath.endsWith("${file}")) {
41
+ return ${JSON.stringify(readFileSync(join(serverDir, file), "utf-8"))};
42
+ }`)
43
+ .join("\n")}
44
+ // js
45
+ process.env.__NEXT_PRIVATE_RUNTIME_TYPE = isAppPath ? 'app' : 'pages';
46
+ try {
47
+ ${jsFiles
48
+ .map((file) => `if (pagePath.endsWith("${file}")) {
49
+ return require(${JSON.stringify(join(serverDir, file))});
50
+ }`)
51
+ .join("\n")}
52
+ } finally {
53
+ process.env.__NEXT_PRIVATE_RUNTIME_TYPE = '';
54
+ }
55
+ `;
56
+ return {
57
+ rule: {
58
+ pattern: `
59
+ function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
60
+ const $_ = getPagePath($$$ARGS);
61
+ $$$_BODY
62
+ }`,
63
+ },
64
+ fix: `
65
+ function requirePage($PAGE, $DIST_DIR, $IS_APPP_ATH) {
66
+ const pagePath = getPagePath($$$ARGS);
67
+ ${fnBody}
68
+ }`,
69
+ };
70
+ }
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ESBuild plugin to mark files bundled by wrangler as external.
3
+ *
4
+ * `.wasm` and `.bin` will ultimately be bundled by wrangler.
5
+ * We should only mark them as external in the adapter.
6
+ *
7
+ * However simply marking them as external would copy the import path to the bundle,
8
+ * i.e. `import("./file.wasm?module")` and given than the bundle is generated in a
9
+ * different location than the input files, the relative path would not be valid.
10
+ *
11
+ * This ESBuild plugin convert relative paths to absolute paths so that they are
12
+ * still valid from inside the bundle.
13
+ *
14
+ * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
+ */
16
+ import type { PluginBuild } from "esbuild";
17
+ export default function setWranglerExternal(): {
18
+ name: string;
19
+ setup: (build: PluginBuild) => Promise<void>;
20
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ESBuild plugin to mark files bundled by wrangler as external.
3
+ *
4
+ * `.wasm` and `.bin` will ultimately be bundled by wrangler.
5
+ * We should only mark them as external in the adapter.
6
+ *
7
+ * However simply marking them as external would copy the import path to the bundle,
8
+ * i.e. `import("./file.wasm?module")` and given than the bundle is generated in a
9
+ * different location than the input files, the relative path would not be valid.
10
+ *
11
+ * This ESBuild plugin convert relative paths to absolute paths so that they are
12
+ * still valid from inside the bundle.
13
+ *
14
+ * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
+ */
16
+ import { dirname, resolve } from "node:path";
17
+ export default function setWranglerExternal() {
18
+ return {
19
+ name: "wrangler-externals",
20
+ setup: async (build) => {
21
+ const namespace = "wrangler-externals-plugin";
22
+ build.onResolve({ filter: /(\.bin|\.wasm\?module)$/ }, ({ path, importer }) => {
23
+ return {
24
+ path: resolve(dirname(importer), path),
25
+ namespace,
26
+ external: true,
27
+ };
28
+ });
29
+ build.onLoad({ filter: /.*/, namespace }, async ({ path }) => {
30
+ return {
31
+ contents: `export * from '${path}';`,
32
+ };
33
+ });
34
+ },
35
+ };
36
+ }
@@ -1,8 +1,6 @@
1
1
  export * from "./inline-eval-manifest.js";
2
2
  export * from "./inline-middleware-manifest-require.js";
3
- export * from "./inline-next-require.js";
4
3
  export * from "./patch-exception-bubbling.js";
5
4
  export * from "./patch-find-dir.js";
6
5
  export * from "./patch-load-instrumentation-module.js";
7
6
  export * from "./patch-read-file.js";
8
- export * from "./wrangler-deps.js";
@@ -1,8 +1,6 @@
1
1
  export * from "./inline-eval-manifest.js";
2
2
  export * from "./inline-middleware-manifest-require.js";
3
- export * from "./inline-next-require.js";
4
3
  export * from "./patch-exception-bubbling.js";
5
4
  export * from "./patch-find-dir.js";
6
5
  export * from "./patch-load-instrumentation-module.js";
7
6
  export * from "./patch-read-file.js";
8
- 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.0",
4
+ "version": "0.4.2",
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.106.0"
74
74
  },
75
75
  "scripts": {
76
76
  "clean": "rimraf dist",
@@ -1,6 +0,0 @@
1
- import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
- /**
3
- * The following avoid various Next.js specific files `require`d at runtime since we can just read
4
- * and inline their content during build time
5
- */
6
- export declare function inlineNextRequire(code: string, buildOpts: BuildOptions): string;
@@ -1,40 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
- /**
5
- * The following avoid various Next.js specific files `require`d at runtime since we can just read
6
- * and inline their content during build time
7
- */
8
- // TODO(vicb): __NEXT_PRIVATE_RUNTIME_TYPE is not handled by this patch
9
- export function inlineNextRequire(code, buildOpts) {
10
- const { outputDir } = buildOpts;
11
- const serverDir = join(outputDir, "server-functions/default", getPackagePath(buildOpts), ".next/server");
12
- const pagesManifestFile = join(serverDir, "pages-manifest.json");
13
- const appPathsManifestFile = join(serverDir, "app-paths-manifest.json");
14
- const pagesManifests = existsSync(pagesManifestFile)
15
- ? Object.values(JSON.parse(readFileSync(pagesManifestFile, "utf-8")))
16
- : [];
17
- const appPathsManifests = existsSync(appPathsManifestFile)
18
- ? Object.values(JSON.parse(readFileSync(appPathsManifestFile, "utf-8")))
19
- : [];
20
- const manifests = pagesManifests.concat(appPathsManifests);
21
- const htmlPages = manifests.filter((file) => file.endsWith(".html"));
22
- const pageModules = manifests.filter((file) => file.endsWith(".js"));
23
- return code.replace(/const pagePath = getPagePath\(.+?\);/, `$&
24
- ${htmlPages
25
- .map((htmlPage) => `
26
- if (pagePath.endsWith("${htmlPage}")) {
27
- return ${JSON.stringify(readFileSync(join(serverDir, htmlPage), "utf-8"))};
28
- }
29
- `)
30
- .join("\n")}
31
- ${pageModules
32
- .map((module) => `
33
- if (pagePath.endsWith("${module}")) {
34
- return require(${JSON.stringify(join(serverDir, module))});
35
- }
36
- `)
37
- .join("\n")}
38
- throw new Error("Unknown pagePath: " + pagePath);
39
- `);
40
- }
@@ -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
- }