@opennextjs/cloudflare 1.0.1 → 1.0.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.
@@ -26,6 +26,17 @@ export function defineCloudflareConfig(config = {}) {
26
26
  dangerous: {
27
27
  enableCacheInterception,
28
28
  },
29
+ middleware: {
30
+ external: true,
31
+ override: {
32
+ wrapper: "cloudflare-edge",
33
+ converter: "edge",
34
+ proxyExternalRequest: "fetch",
35
+ incrementalCache: resolveIncrementalCache(incrementalCache),
36
+ tagCache: resolveTagCache(tagCache),
37
+ queue: resolveQueue(queue),
38
+ },
39
+ },
29
40
  };
30
41
  }
31
42
  function resolveIncrementalCache(value = "dummy") {
@@ -22,6 +22,7 @@ import { openNextResolvePlugin } from "@opennextjs/aws/plugins/resolve.js";
22
22
  import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
23
23
  import { getOpenNextConfig } from "../../../api/config.js";
24
24
  import { patchResRevalidate } from "../patches/plugins/res-revalidate.js";
25
+ import { patchUseCacheIO } from "../patches/plugins/use-cache.js";
25
26
  import { normalizePath } from "../utils/index.js";
26
27
  import { copyWorkerdPackages } from "../utils/workerd.js";
27
28
  export async function createServerBundle(options, codeCustomization) {
@@ -141,6 +142,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
141
142
  patchBackgroundRevalidation,
142
143
  // Cloudflare specific patches
143
144
  patchResRevalidate,
145
+ patchUseCacheIO,
144
146
  ...additionalCodePatches,
145
147
  ]);
146
148
  // Build Lambda code
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getPackagePath } from "@opennextjs/aws/build/helper.js";
4
4
  import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
5
+ import { normalizePath } from "../../utils/normalize-path.js";
5
6
  export function patchInstrumentation(updater, buildOpts) {
6
7
  const builtInstrumentationPath = getBuiltInstrumentationPath(buildOpts);
7
8
  updater.updateContent("patch-instrumentation-next15", [
@@ -69,7 +70,7 @@ export function getNext14Rule(builtInstrumentationPath) {
69
70
  function getBuiltInstrumentationPath(buildOpts) {
70
71
  const { outputDir } = buildOpts;
71
72
  const maybeBuiltInstrumentationPath = join(outputDir, "server-functions/default", getPackagePath(buildOpts), `.next/server/${INSTRUMENTATION_HOOK_FILENAME}.js`);
72
- return existsSync(maybeBuiltInstrumentationPath) ? maybeBuiltInstrumentationPath : null;
73
+ return existsSync(maybeBuiltInstrumentationPath) ? normalizePath(maybeBuiltInstrumentationPath) : null;
73
74
  }
74
75
  /**
75
76
  * Pattern to detect instrumentation hooks file
@@ -0,0 +1,3 @@
1
+ import type { CodePatcher } from "@opennextjs/aws/build/patch/codePatcher.js";
2
+ export declare const rule = "\nrule:\n kind: if_statement\n inside:\n kind: function_declaration\n stopBy: end\n has:\n kind: identifier\n pattern: createSnapshot\nfix:\n '// Ignored snapshot'\n";
3
+ export declare const patchUseCacheIO: CodePatcher;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * This patch will replace the createSnapshot function in the
3
+ * server/app-render/async-local-storage.js file to an empty string.
4
+ * This is necessary because the createSnapshot function is causing I/O issues for
5
+ * ISR/SSG revalidation in Cloudflare Workers.
6
+ * This is because by default it will use AsyncLocalStorage.snapshot() and it will
7
+ * bind everything to the initial request context.
8
+ * The downsides is that use cache function will have access to the full request
9
+ * ALS context from next (i.e. cookies, headers ...)
10
+ * TODO: Find a better fix for this issue.
11
+ */
12
+ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
13
+ import { getCrossPlatformPathRegex } from "@opennextjs/aws/utils/regex.js";
14
+ export const rule = `
15
+ rule:
16
+ kind: if_statement
17
+ inside:
18
+ kind: function_declaration
19
+ stopBy: end
20
+ has:
21
+ kind: identifier
22
+ pattern: createSnapshot
23
+ fix:
24
+ '// Ignored snapshot'
25
+ `;
26
+ export const patchUseCacheIO = {
27
+ name: "patch-use-cache",
28
+ patches: [
29
+ {
30
+ versions: ">=15.3.1",
31
+ field: {
32
+ pathFilter: getCrossPlatformPathRegex(String.raw `server/app-render/async-local-storage\.js$`, {
33
+ escape: false,
34
+ }),
35
+ contentFilter: /createSnapshot/,
36
+ patchCode: async ({ code }) => patchCode(code, rule),
37
+ },
38
+ },
39
+ ],
40
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,156 @@
1
+ import { patchCode } from "@opennextjs/aws/build/patch/astCodePatcher.js";
2
+ import { expect, test } from "vitest";
3
+ import { rule } from "./use-cache.js";
4
+ const codeToPatch = `"use strict";
5
+ Object.defineProperty(exports, "__esModule", {
6
+ value: true
7
+ });
8
+ 0 && (module.exports = {
9
+ bindSnapshot: null,
10
+ createAsyncLocalStorage: null,
11
+ createSnapshot: null
12
+ });
13
+ function _export(target, all) {
14
+ for(var name in all)Object.defineProperty(target, name, {
15
+ enumerable: true,
16
+ get: all[name]
17
+ });
18
+ }
19
+ _export(exports, {
20
+ bindSnapshot: function() {
21
+ return bindSnapshot;
22
+ },
23
+ createAsyncLocalStorage: function() {
24
+ return createAsyncLocalStorage;
25
+ },
26
+ createSnapshot: function() {
27
+ return createSnapshot;
28
+ }
29
+ });
30
+ const sharedAsyncLocalStorageNotAvailableError = Object.defineProperty(new Error('Invariant: AsyncLocalStorage accessed in runtime where it is not available'), "__NEXT_ERROR_CODE", {
31
+ value: "E504",
32
+ enumerable: false,
33
+ configurable: true
34
+ });
35
+ class FakeAsyncLocalStorage {
36
+ disable() {
37
+ throw sharedAsyncLocalStorageNotAvailableError;
38
+ }
39
+ getStore() {
40
+ // This fake implementation of AsyncLocalStorage always returns \`undefined\`.
41
+ return undefined;
42
+ }
43
+ run() {
44
+ throw sharedAsyncLocalStorageNotAvailableError;
45
+ }
46
+ exit() {
47
+ throw sharedAsyncLocalStorageNotAvailableError;
48
+ }
49
+ enterWith() {
50
+ throw sharedAsyncLocalStorageNotAvailableError;
51
+ }
52
+ static bind(fn) {
53
+ return fn;
54
+ }
55
+ }
56
+ const maybeGlobalAsyncLocalStorage = typeof globalThis !== 'undefined' && globalThis.AsyncLocalStorage;
57
+ function createAsyncLocalStorage() {
58
+ if (maybeGlobalAsyncLocalStorage) {
59
+ return new maybeGlobalAsyncLocalStorage();
60
+ }
61
+ return new FakeAsyncLocalStorage();
62
+ }
63
+ function bindSnapshot(fn) {
64
+ if (maybeGlobalAsyncLocalStorage) {
65
+ return maybeGlobalAsyncLocalStorage.bind(fn);
66
+ }
67
+ return FakeAsyncLocalStorage.bind(fn);
68
+ }
69
+ function createSnapshot() {
70
+ if (maybeGlobalAsyncLocalStorage) {
71
+ return maybeGlobalAsyncLocalStorage.snapshot();
72
+ }
73
+ return function(fn, ...args) {
74
+ return fn(...args);
75
+ };
76
+ }
77
+
78
+ //# sourceMappingURL=async-local-storage.js.map
79
+ `;
80
+ test("patch the createSnapshot function", () => {
81
+ const patchedCode = patchCode(codeToPatch, rule);
82
+ expect(patchedCode).toMatchInlineSnapshot(`""use strict";
83
+ Object.defineProperty(exports, "__esModule", {
84
+ value: true
85
+ });
86
+ 0 && (module.exports = {
87
+ bindSnapshot: null,
88
+ createAsyncLocalStorage: null,
89
+ createSnapshot: null
90
+ });
91
+ function _export(target, all) {
92
+ for(var name in all)Object.defineProperty(target, name, {
93
+ enumerable: true,
94
+ get: all[name]
95
+ });
96
+ }
97
+ _export(exports, {
98
+ bindSnapshot: function() {
99
+ return bindSnapshot;
100
+ },
101
+ createAsyncLocalStorage: function() {
102
+ return createAsyncLocalStorage;
103
+ },
104
+ createSnapshot: function() {
105
+ return createSnapshot;
106
+ }
107
+ });
108
+ const sharedAsyncLocalStorageNotAvailableError = Object.defineProperty(new Error('Invariant: AsyncLocalStorage accessed in runtime where it is not available'), "__NEXT_ERROR_CODE", {
109
+ value: "E504",
110
+ enumerable: false,
111
+ configurable: true
112
+ });
113
+ class FakeAsyncLocalStorage {
114
+ disable() {
115
+ throw sharedAsyncLocalStorageNotAvailableError;
116
+ }
117
+ getStore() {
118
+ // This fake implementation of AsyncLocalStorage always returns \`undefined\`.
119
+ return undefined;
120
+ }
121
+ run() {
122
+ throw sharedAsyncLocalStorageNotAvailableError;
123
+ }
124
+ exit() {
125
+ throw sharedAsyncLocalStorageNotAvailableError;
126
+ }
127
+ enterWith() {
128
+ throw sharedAsyncLocalStorageNotAvailableError;
129
+ }
130
+ static bind(fn) {
131
+ return fn;
132
+ }
133
+ }
134
+ const maybeGlobalAsyncLocalStorage = typeof globalThis !== 'undefined' && globalThis.AsyncLocalStorage;
135
+ function createAsyncLocalStorage() {
136
+ if (maybeGlobalAsyncLocalStorage) {
137
+ return new maybeGlobalAsyncLocalStorage();
138
+ }
139
+ return new FakeAsyncLocalStorage();
140
+ }
141
+ function bindSnapshot(fn) {
142
+ if (maybeGlobalAsyncLocalStorage) {
143
+ return maybeGlobalAsyncLocalStorage.bind(fn);
144
+ }
145
+ return FakeAsyncLocalStorage.bind(fn);
146
+ }
147
+ function createSnapshot() {
148
+ // Ignored snapshot
149
+ return function(fn, ...args) {
150
+ return fn(...args);
151
+ };
152
+ }
153
+
154
+ //# sourceMappingURL=async-local-storage.js.map
155
+ "`);
156
+ });
@@ -14,6 +14,7 @@
14
14
  * ref: https://developers.cloudflare.com/workers/wrangler/bundling/
15
15
  */
16
16
  import { dirname, resolve } from "node:path";
17
+ import { normalizePath } from "../../utils/normalize-path.js";
17
18
  export function setWranglerExternal() {
18
19
  return {
19
20
  name: "wrangler-externals",
@@ -22,7 +23,7 @@ export function setWranglerExternal() {
22
23
  //TODO: Ideally in the future we would like to analyze the files in case they are using wasm in a Node way (i.e. WebAssembly.instantiate)
23
24
  build.onResolve({ filter: /(\.bin|\.wasm(\?module)?)$/ }, ({ path, importer }) => {
24
25
  return {
25
- path: resolve(dirname(importer), path),
26
+ path: normalizePath(resolve(dirname(importer), path)),
26
27
  namespace,
27
28
  external: true,
28
29
  };
@@ -6,6 +6,7 @@ import logger from "@opennextjs/aws/logger.js";
6
6
  */
7
7
  export function ensureCloudflareConfig(config) {
8
8
  const requirements = {
9
+ // Check for the default function
9
10
  dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
10
11
  dftUseEdgeConverter: config.default?.override?.converter === "edge",
11
12
  dftUseFetchProxy: config.default?.override?.proxyExternalRequest === "fetch",
@@ -16,7 +17,11 @@ export function ensureCloudflareConfig(config) {
16
17
  dftMaybeUseQueue: config.default?.override?.queue === "dummy" ||
17
18
  config.default?.override?.queue === "direct" ||
18
19
  typeof config.default?.override?.queue === "function",
19
- mwIsMiddlewareIntegrated: config.middleware === undefined,
20
+ // Check for the middleware function
21
+ mwIsMiddlewareExternal: config.middleware?.external === true,
22
+ mwUseCloudflareWrapper: config.middleware?.override?.wrapper === "cloudflare-edge",
23
+ mwUseEdgeConverter: config.middleware?.override?.converter === "edge",
24
+ mwUseFetchProxy: config.middleware?.override?.proxyExternalRequest === "fetch",
20
25
  hasCryptoExternal: config.edgeExternals?.includes("node:crypto"),
21
26
  };
22
27
  if (config.default?.override?.queue === "direct") {
@@ -31,11 +36,22 @@ export function ensureCloudflareConfig(config) {
31
36
  converter: "edge",
32
37
  proxyExternalRequest: "fetch",
33
38
  incrementalCache: "dummy" | function,
34
- tagCache: "dummy",
39
+ tagCache: "dummy" | function,
35
40
  queue: "dummy" | "direct" | function,
36
41
  },
37
42
  },
38
43
  edgeExternals: ["node:crypto"],
44
+ middleware: {
45
+ external: true,
46
+ override: {
47
+ wrapper: "cloudflare-edge",
48
+ converter: "edge",
49
+ proxyExternalRequest: "fetch",
50
+ incrementalCache: "dummy" | function,
51
+ tagCache: "dummy" | function,
52
+ queue: "dummy" | "direct" | function,
53
+ },
54
+ },
39
55
  }\n\n`.replace(/^ {8}/gm, ""));
40
56
  }
41
57
  }
@@ -9,6 +9,7 @@ import { BINDING_NAME as R2_CACHE_BINDING_NAME, NAME as R2_CACHE_NAME, PREFIX_EN
9
9
  import { CACHE_DIR as STATIC_ASSETS_CACHE_DIR, NAME as STATIC_ASSETS_CACHE_NAME, } from "../../api/overrides/incremental-cache/static-assets-incremental-cache.js";
10
10
  import { computeCacheKey } from "../../api/overrides/internal.js";
11
11
  import { BINDING_NAME as D1_TAG_BINDING_NAME, NAME as D1_TAG_NAME, } from "../../api/overrides/tag-cache/d1-next-tag-cache.js";
12
+ import { normalizePath } from "../build/utils/normalize-path.js";
12
13
  import { runWrangler } from "../utils/run-wrangler.js";
13
14
  async function resolveCacheName(value) {
14
15
  return typeof value === "function" ? (await value()).name : value;
@@ -20,8 +21,8 @@ export function getCacheAssets(opts) {
20
21
  }).filter((f) => f.isFile());
21
22
  const assets = [];
22
23
  for (const file of allFiles) {
23
- const fullPath = file.fullpathPosix();
24
- const relativePath = path.relative(path.join(opts.outputDir, "cache"), fullPath);
24
+ const fullPath = file.fullpath();
25
+ const relativePath = normalizePath(path.relative(path.join(opts.outputDir, "cache"), fullPath));
25
26
  if (relativePath.startsWith("__fetch")) {
26
27
  const [__fetch, buildId, ...keyParts] = relativePath.split("/");
27
28
  if (__fetch !== "__fetch" || buildId === undefined || keyParts.length === 0) {
@@ -74,7 +75,11 @@ async function populateR2IncrementalCache(options, populateCacheOptions) {
74
75
  buildId,
75
76
  cacheType: isFetch ? "fetch" : "cache",
76
77
  });
77
- runWrangler(options, ["r2 object put", quoteShellMeta(path.join(bucket, cacheKey)), `--file ${quoteShellMeta(fullPath)}`],
78
+ runWrangler(options, [
79
+ "r2 object put",
80
+ quoteShellMeta(normalizePath(path.join(bucket, cacheKey))),
81
+ `--file ${quoteShellMeta(fullPath)}`,
82
+ ],
78
83
  // NOTE: R2 does not support the environment flag and results in the following error:
79
84
  // Incorrect type for the 'cacheExpiry' field on 'HttpMetadata': the provided value is not of type 'date'.
80
85
  { target: populateCacheOptions.target, logging: "error" });
@@ -75,6 +75,10 @@ function initRuntime() {
75
75
  Request: CustomRequest,
76
76
  __BUILD_TIMESTAMP_MS__: __BUILD_TIMESTAMP_MS__,
77
77
  __NEXT_BASE_PATH__: __NEXT_BASE_PATH__,
78
+ // The external middleware will use the convertTo function of the `edge` converter
79
+ // by default it will try to fetch the request, but since we are running everything in the same worker
80
+ // we need to use the request as is.
81
+ __dangerous_ON_edge_converter_returns_request: true,
78
82
  });
79
83
  }
80
84
  /**
@@ -1,5 +1,7 @@
1
1
  //@ts-expect-error: Will be resolved by wrangler build
2
2
  import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
3
+ // @ts-expect-error: Will be resolved by wrangler build
4
+ import { handler as middlewareHandler } from "./middleware/handler.mjs";
3
5
  //@ts-expect-error: Will be resolved by wrangler build
4
6
  export { DOQueueHandler } from "./.build/durable-objects/queue.js";
5
7
  //@ts-expect-error: Will be resolved by wrangler build
@@ -27,9 +29,14 @@ export default {
27
29
  ? env.ASSETS?.fetch(`http://assets.local${imageUrl}`)
28
30
  : fetch(imageUrl, { cf: { cacheEverything: true } });
29
31
  }
32
+ // - `Request`s are handled by the Next server
33
+ const reqOrResp = await middlewareHandler(request, env, ctx);
34
+ if (reqOrResp instanceof Response) {
35
+ return reqOrResp;
36
+ }
30
37
  // @ts-expect-error: resolved by wrangler build
31
38
  const { handler } = await import("./server-functions/default/handler.mjs");
32
- return handler(request, env, ctx);
39
+ return handler(reqOrResp, env, ctx);
33
40
  });
34
41
  },
35
42
  };
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": "1.0.1",
4
+ "version": "1.0.3",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"
@@ -43,7 +43,7 @@
43
43
  "homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
44
44
  "dependencies": {
45
45
  "@dotenvx/dotenvx": "1.31.0",
46
- "@opennextjs/aws": "~3.6.0",
46
+ "@opennextjs/aws": "^3.6.1",
47
47
  "enquirer": "^2.4.1",
48
48
  "glob": "^11.0.0",
49
49
  "ts-tqdm": "^0.8.6"
@@ -13,9 +13,10 @@
13
13
  // See https://opennext.js.org/cloudflare/caching
14
14
  {
15
15
  "binding": "NEXT_INC_CACHE_R2_BUCKET",
16
- // Create a bucket before deploying
16
+ // Create the bucket before deploying
17
+ // You can change the bucket name if you want
17
18
  // See https://developers.cloudflare.com/workers/wrangler/commands/#r2-bucket-create
18
- "bucket_name": "<BUCKET_NAME>"
19
+ "bucket_name": "cache"
19
20
  }
20
21
  ]
21
22
  }