@opennextjs/cloudflare 0.4.3 → 0.4.4

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.
@@ -15,6 +15,17 @@ export function getCloudflareContext() {
15
15
  const global = globalThis;
16
16
  const cloudflareContext = global[cloudflareContextSymbol];
17
17
  if (!cloudflareContext) {
18
+ // For SSG Next.js creates (jest) workers that run in parallel, those don't get the current global
19
+ // state so they can't get access to the cloudflare context, unfortunately there isn't anything we
20
+ // can do about this, so the only solution is to error asking the developer to opt-out of SSG
21
+ // Next.js sets globalThis.__NEXT_DATA__.nextExport to true for the worker, so we can use that to detect
22
+ // that the route is being SSG'd (source: https://github.com/vercel/next.js/blob/4e394608423/packages/next/src/export/worker.ts#L55-L57)
23
+ if (global.__NEXT_DATA__?.nextExport === true) {
24
+ throw new Error(`\n\nERROR: \`getCloudflareContext\` has been called in a static route` +
25
+ ` that is not allowed, please either avoid calling \`getCloudflareContext\`` +
26
+ ` in the route or make the route non static (for example by exporting the` +
27
+ ` \`dynamic\` route segment config set to \`'force-dynamic'\`.\n`);
28
+ }
18
29
  // the cloudflare context is initialized by the worker and is always present in production/preview
19
30
  // during local development (`next dev`) it might be missing only if the developers hasn't called
20
31
  // the `initOpenNextCloudflareForDev` function in their Next.js config file
@@ -13,6 +13,7 @@ import { compileEnvFiles } from "./open-next/compile-env-files.js";
13
13
  import { copyCacheAssets } from "./open-next/copyCacheAssets.js";
14
14
  import { createServerBundle } from "./open-next/createServerBundle.js";
15
15
  import { createOpenNextConfigIfNotExistent, createWranglerConfigIfNotExistent, ensureCloudflareConfig, } from "./utils/index.js";
16
+ import { getVersion } from "./utils/version.js";
16
17
  /**
17
18
  * Builds the application in a format that can be passed to workerd
18
19
  *
@@ -40,7 +41,9 @@ export async function build(projectOpts) {
40
41
  logger.info(`App directory: ${options.appPath}`);
41
42
  buildHelper.printNextjsVersion(options);
42
43
  ensureNextjsVersionSupported(options);
43
- buildHelper.printOpenNextVersion(options);
44
+ const { aws, cloudflare } = getVersion();
45
+ logger.info(`@opennextjs/cloudflare version: ${cloudflare}`);
46
+ logger.info(`@opennextjs/aws version: ${aws}`);
44
47
  if (projectOpts.skipNextBuild) {
45
48
  logger.warn("Skipping Next.js build");
46
49
  }
@@ -2,16 +2,15 @@ import fs from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { Lang, parse } from "@ast-grep/napi";
6
5
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
7
6
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
8
7
  import { build } from "esbuild";
9
- import { patchOptionalDependencies } from "./patches/ast/optional-deps.js";
10
8
  import { patchVercelOgLibrary } from "./patches/ast/patch-vercel-og-library.js";
11
9
  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";
10
+ import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
11
+ import { fixRequire } from "./patches/plugins/require.js";
12
+ import { inlineRequirePagePlugin } from "./patches/plugins/require-page.js";
13
+ import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
15
14
  import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
16
15
  /** The dist directory of the Cloudflare adapter package */
17
16
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
@@ -56,8 +55,9 @@ export async function bundleServer(buildOpts) {
56
55
  inlineRequirePagePlugin(buildOpts),
57
56
  setWranglerExternal(),
58
57
  fixRequire(),
58
+ handleOptionalDependencies(optionalDependencies),
59
59
  ],
60
- external: ["./middleware/handler.mjs", ...optionalDependencies],
60
+ external: ["./middleware/handler.mjs"],
61
61
  alias: {
62
62
  // Note: we apply an empty shim to next/dist/compiled/ws because it generates two `eval`s:
63
63
  // eval("require")("bufferutil");
@@ -74,7 +74,7 @@ export async function bundleServer(buildOpts) {
74
74
  },
75
75
  define: {
76
76
  // config file used by Next.js, see: https://github.com/vercel/next.js/blob/68a7128/packages/next/src/build/utils.ts#L2137-L2139
77
- "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": `${JSON.stringify(nextConfig)}`,
77
+ "process.env.__NEXT_PRIVATE_STANDALONE_CONFIG": JSON.stringify(JSON.stringify(nextConfig)),
78
78
  // Next.js tried to access __dirname so we need to define it
79
79
  __dirname: '""',
80
80
  // Note: we need the __non_webpack_require__ variable declared as it is used by next-server:
@@ -168,9 +168,7 @@ export async function updateWorkerBundledCode(workerOutputFile, buildOpts) {
168
168
  (code) => code.replace('require.resolve("./cache.cjs")', '"unused"'),
169
169
  ],
170
170
  ]);
171
- const bundle = parse(Lang.TypeScript, patchedCode).root();
172
- const { edits } = patchOptionalDependencies(bundle, optionalDependencies);
173
- await writeFile(workerOutputFile, bundle.commitEdits(edits));
171
+ await writeFile(workerOutputFile, patchedCode);
174
172
  }
175
173
  function createFixRequiresESBuildPlugin(options) {
176
174
  return {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ESBuild plugin to handle optional dependencies.
3
+ *
4
+ * Optional dependencies might be installed by the application to support optional features.
5
+ *
6
+ * When an optional dependency is installed, it must be inlined in the bundle.
7
+ * When it is not installed, the plugin swaps it for a throwing implementation.
8
+ *
9
+ * The plugin uses ESBuild built-in resolution to check if the dependency is installed.
10
+ */
11
+ import type { PluginBuild } from "esbuild";
12
+ export declare function handleOptionalDependencies(dependencies: string[]): {
13
+ name: string;
14
+ setup: (build: PluginBuild) => Promise<void>;
15
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ESBuild plugin to handle optional dependencies.
3
+ *
4
+ * Optional dependencies might be installed by the application to support optional features.
5
+ *
6
+ * When an optional dependency is installed, it must be inlined in the bundle.
7
+ * When it is not installed, the plugin swaps it for a throwing implementation.
8
+ *
9
+ * The plugin uses ESBuild built-in resolution to check if the dependency is installed.
10
+ */
11
+ export function handleOptionalDependencies(dependencies) {
12
+ // Regex matching either a full module ("module") or a prefix ("module/...")
13
+ const filter = new RegExp(`^(${dependencies.flatMap((name) => [`${name}$`, String.raw `${name}/`]).join("|")})`);
14
+ const name = "optional-deps";
15
+ const marker = {};
16
+ const nsMissingDependency = `${name}-missing-dependency`;
17
+ return {
18
+ name,
19
+ setup: async (build) => {
20
+ build.onResolve({ filter }, async ({ path, pluginData, ...options }) => {
21
+ // Use ESBuild to resolve the dependency.
22
+ // Because the plugin asks ESBuild to resolve the path we just received,
23
+ // ESBuild will ask this plugin again.
24
+ // We use a marker in the pluginData to break the loop.
25
+ if (pluginData === marker) {
26
+ return {};
27
+ }
28
+ const result = await build.resolve(path, {
29
+ ...options,
30
+ pluginData: marker,
31
+ });
32
+ // ESBuild reports error when the dependency is not installed.
33
+ // In such a case the OnLoad hook will inline a throwing implementation.
34
+ if (result.errors.length > 0) {
35
+ return {
36
+ path: `/${path}`,
37
+ namespace: nsMissingDependency,
38
+ pluginData: { name: path },
39
+ };
40
+ }
41
+ // Returns ESBuild resolution information when the dependency is installed.
42
+ return result;
43
+ });
44
+ // Replaces missing dependency with a throwing implementation.
45
+ build.onLoad({ filter: /.*/, namespace: nsMissingDependency }, ({ pluginData }) => {
46
+ return {
47
+ contents: `throw new Error('Missing optional dependency "${pluginData.name}"')`,
48
+ };
49
+ });
50
+ },
51
+ };
52
+ }
@@ -1,6 +1,6 @@
1
1
  import { type BuildOptions } from "@opennextjs/aws/build/helper.js";
2
2
  import type { PluginBuild } from "esbuild";
3
- export default function inlineRequirePagePlugin(buildOpts: BuildOptions): {
3
+ export declare function inlineRequirePagePlugin(buildOpts: BuildOptions): {
4
4
  name: string;
5
5
  setup: (build: PluginBuild) => Promise<void>;
6
6
  };
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
5
5
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
6
6
  import { patchCode } from "../ast/util.js";
7
- export default function inlineRequirePagePlugin(buildOpts) {
7
+ export function inlineRequirePagePlugin(buildOpts) {
8
8
  return {
9
9
  name: "inline-require-page",
10
10
  setup: async (build) => {
@@ -1,5 +1,5 @@
1
1
  import type { PluginBuild } from "esbuild";
2
- export default function fixRequire(): {
2
+ export declare function fixRequire(): {
3
3
  name: string;
4
4
  setup: (build: PluginBuild) => Promise<void>;
5
5
  };
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
- export default function fixRequire() {
2
+ export function fixRequire() {
3
3
  return {
4
4
  name: "fix-require",
5
5
  setup: async (build) => {
6
- build.onLoad({ filter: /.*/ }, async ({ path }) => {
6
+ build.onLoad({ filter: /\.(js|mjs|cjs|jsx|ts|tsx)$/ }, async ({ path }) => {
7
7
  let contents = await fs.readFile(path, "utf-8");
8
8
  // `eval(...)` is not supported by workerd.
9
9
  contents = contents.replaceAll(`eval("require")`, "require");
@@ -14,7 +14,7 @@
14
14
  * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
15
  */
16
16
  import type { PluginBuild } from "esbuild";
17
- export default function setWranglerExternal(): {
17
+ export declare function setWranglerExternal(): {
18
18
  name: string;
19
19
  setup: (build: PluginBuild) => Promise<void>;
20
20
  };
@@ -14,7 +14,7 @@
14
14
  * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
15
  */
16
16
  import { dirname, resolve } from "node:path";
17
- export default function setWranglerExternal() {
17
+ export function setWranglerExternal() {
18
18
  return {
19
19
  name: "wrangler-externals",
20
20
  setup: async (build) => {
@@ -0,0 +1,4 @@
1
+ export declare function getVersion(): {
2
+ cloudflare: any;
3
+ aws: any;
4
+ };
@@ -0,0 +1,12 @@
1
+ import { createRequire } from "node:module";
2
+ import { join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export function getVersion() {
5
+ const require = createRequire(import.meta.url);
6
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
7
+ const pkgJson = require(join(__dirname, "../../../../package.json"));
8
+ return {
9
+ cloudflare: pkgJson.version,
10
+ aws: pkgJson.dependencies["@opennextjs/aws"],
11
+ };
12
+ }
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.3",
4
+ "version": "0.4.4",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -63,7 +63,7 @@
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@715",
66
+ "@opennextjs/aws": "https://pkg.pr.new/@opennextjs/aws@722",
67
67
  "enquirer": "^2.4.1",
68
68
  "glob": "^11.0.0",
69
69
  "ts-morph": "^23.0.0",
@@ -1,20 +0,0 @@
1
- import { type SgNode } from "@ast-grep/napi";
2
- /**
3
- * Handles optional dependencies.
4
- *
5
- * A top level `require(optionalDep)` would throw when the dep is not installed.
6
- *
7
- * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
- */
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[]): {
18
- edits: import("@ast-grep/napi").Edit[];
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>>[];
20
- };
@@ -1,44 +0,0 @@
1
- import { applyRule } from "./util.js";
2
- /**
3
- * Handles optional dependencies.
4
- *
5
- * A top level `require(optionalDep)` would throw when the dep is not installed.
6
- *
7
- * So we wrap `require(optionalDep)` in a try/catch (if not already present).
8
- */
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
- stopBy: end
21
- regex: ${regex}
22
- not:
23
- inside:
24
- kind: try_statement
25
- stopBy: end
26
-
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);
44
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,135 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { buildOptionalDepRule } from "./optional-deps.js";
3
- import { patchCode } from "./util.js";
4
- describe("optional dependecy", () => {
5
- it('should wrap a top-level require("caniuse-lite") in a try-catch', () => {
6
- const code = `t = require("caniuse-lite");`;
7
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
8
- "try {
9
- t = require("caniuse-lite");
10
- } catch {
11
- throw new Error('The optional dependency "caniuse-lite" is not installed');
12
- };"
13
- `);
14
- });
15
- it('should wrap a top-level require("caniuse-lite/data") in a try-catch', () => {
16
- const code = `t = require("caniuse-lite/data");`;
17
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
18
- "try {
19
- t = require("caniuse-lite/data");
20
- } catch {
21
- throw new Error('The optional dependency "caniuse-lite/data" is not installed');
22
- };"
23
- `);
24
- });
25
- it('should wrap e.exports = require("caniuse-lite") in a try-catch', () => {
26
- const code = 'e.exports = require("caniuse-lite");';
27
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
28
- "try {
29
- e.exports = require("caniuse-lite");
30
- } catch {
31
- throw new Error('The optional dependency "caniuse-lite" is not installed');
32
- };"
33
- `);
34
- });
35
- it('should wrap module.exports = require("caniuse-lite") in a try-catch', () => {
36
- const code = 'module.exports = require("caniuse-lite");';
37
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
38
- "try {
39
- module.exports = require("caniuse-lite");
40
- } catch {
41
- throw new Error('The optional dependency "caniuse-lite" is not installed');
42
- };"
43
- `);
44
- });
45
- it('should wrap exports.foo = require("caniuse-lite") in a try-catch', () => {
46
- const code = 'exports.foo = require("caniuse-lite");';
47
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
48
- "try {
49
- exports.foo = require("caniuse-lite");
50
- } catch {
51
- throw new Error('The optional dependency "caniuse-lite" is not installed');
52
- };"
53
- `);
54
- });
55
- it('should not wrap require("lodash") in a try-catch', () => {
56
- const code = 't = require("lodash");';
57
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("lodash");"`);
58
- });
59
- it('should not wrap require("other-module") if it does not match caniuse-lite regex', () => {
60
- const code = 't = require("other-module");';
61
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`"t = require("other-module");"`);
62
- });
63
- it("should not wrap a require() call already inside a try-catch", () => {
64
- const code = `
65
- try {
66
- t = require("caniuse-lite");
67
- } catch {}
68
- `;
69
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
70
- "try {
71
- t = require("caniuse-lite");
72
- } catch {}
73
- "
74
- `);
75
- });
76
- it("should handle require with subpath and not wrap if already in try-catch", () => {
77
- const code = `
78
- try {
79
- t = require("caniuse-lite/path");
80
- } catch {}
81
- `;
82
- expect(patchCode(code, buildOptionalDepRule(["caniuse-lite"]))).toMatchInlineSnapshot(`
83
- "try {
84
- t = require("caniuse-lite/path");
85
- } catch {}
86
- "
87
- `);
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
- });
135
- });