@opennextjs/cloudflare 0.5.8 → 0.5.10

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.
@@ -4,6 +4,7 @@ declare global {
4
4
  NEXT_CACHE_D1?: D1Database;
5
5
  NEXT_CACHE_D1_TAGS_TABLE?: string;
6
6
  NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string;
7
+ NEXT_CACHE_REVALIDATION_WORKER?: Service;
7
8
  ASSETS?: Fetcher;
8
9
  }
9
10
  }
@@ -4,6 +4,8 @@ export declare const DEFAULT_REVALIDATION_TIMEOUT_MS = 10000;
4
4
  * The Memory Queue offers basic ISR revalidation by directly requesting a revalidation of a route.
5
5
  *
6
6
  * It offers basic support for in-memory de-duping per isolate.
7
+ *
8
+ * A service binding called `NEXT_CACHE_REVALIDATION_WORKER` that points to your worker is required.
7
9
  */
8
10
  export declare class MemoryQueue implements Queue {
9
11
  private opts;
@@ -1,9 +1,13 @@
1
1
  import logger from "@opennextjs/aws/logger.js";
2
+ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
3
+ import { getCloudflareContext } from "./cloudflare-context";
2
4
  export const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
3
5
  /**
4
6
  * The Memory Queue offers basic ISR revalidation by directly requesting a revalidation of a route.
5
7
  *
6
8
  * It offers basic support for in-memory de-duping per isolate.
9
+ *
10
+ * A service binding called `NEXT_CACHE_REVALIDATION_WORKER` that points to your worker is required.
7
11
  */
8
12
  export class MemoryQueue {
9
13
  opts;
@@ -13,6 +17,9 @@ export class MemoryQueue {
13
17
  this.opts = opts;
14
18
  }
15
19
  async send({ MessageBody: { host, url }, MessageGroupId }) {
20
+ const service = getCloudflareContext().env.NEXT_CACHE_REVALIDATION_WORKER;
21
+ if (!service)
22
+ throw new IgnorableError("No service binding for cache revalidation worker");
16
23
  if (this.revalidatedPaths.has(MessageGroupId))
17
24
  return;
18
25
  this.revalidatedPaths.set(MessageGroupId,
@@ -23,7 +30,7 @@ export class MemoryQueue {
23
30
  // TODO: Drop the import - https://github.com/opennextjs/opennextjs-cloudflare/issues/361
24
31
  // @ts-ignore
25
32
  const manifest = await import("./.next/prerender-manifest.json");
26
- await globalThis.internalFetch(`${protocol}://${host}${url}`, {
33
+ await service.fetch(`${protocol}://${host}${url}`, {
27
34
  method: "HEAD",
28
35
  headers: {
29
36
  "x-prerender-revalidate": manifest.preview.previewModeId,
@@ -1,7 +1,13 @@
1
1
  import { generateMessageGroupId } from "@opennextjs/aws/core/routing/queue.js";
2
2
  import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
3
- import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue";
3
+ import cache, { DEFAULT_REVALIDATION_TIMEOUT_MS } from "./memory-queue.js";
4
4
  vi.mock("./.next/prerender-manifest.json", () => Promise.resolve({ preview: { previewModeId: "id" } }));
5
+ const mockServiceWorkerFetch = vi.fn();
6
+ vi.mock("./cloudflare-context", () => ({
7
+ getCloudflareContext: () => ({
8
+ env: { NEXT_CACHE_REVALIDATION_WORKER: { fetch: mockServiceWorkerFetch } },
9
+ }),
10
+ }));
5
11
  describe("MemoryQueue", () => {
6
12
  beforeAll(() => {
7
13
  vi.useFakeTimers();
@@ -16,7 +22,7 @@ describe("MemoryQueue", () => {
16
22
  });
17
23
  vi.advanceTimersByTime(DEFAULT_REVALIDATION_TIMEOUT_MS);
18
24
  await firstRequest;
19
- expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
25
+ expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
20
26
  const secondRequest = cache.send({
21
27
  MessageBody: { host: "test.local", url: "/test" },
22
28
  MessageGroupId: generateMessageGroupId("/test"),
@@ -24,7 +30,7 @@ describe("MemoryQueue", () => {
24
30
  });
25
31
  vi.advanceTimersByTime(1);
26
32
  await secondRequest;
27
- expect(globalThis.internalFetch).toHaveBeenCalledTimes(2);
33
+ expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(2);
28
34
  });
29
35
  it("should process revalidations for multiple paths", async () => {
30
36
  const firstRequest = cache.send({
@@ -34,7 +40,7 @@ describe("MemoryQueue", () => {
34
40
  });
35
41
  vi.advanceTimersByTime(1);
36
42
  await firstRequest;
37
- expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
43
+ expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
38
44
  const secondRequest = cache.send({
39
45
  MessageBody: { host: "test.local", url: "/test" },
40
46
  MessageGroupId: generateMessageGroupId("/other"),
@@ -42,7 +48,7 @@ describe("MemoryQueue", () => {
42
48
  });
43
49
  vi.advanceTimersByTime(1);
44
50
  await secondRequest;
45
- expect(globalThis.internalFetch).toHaveBeenCalledTimes(2);
51
+ expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(2);
46
52
  });
47
53
  it("should de-dupe revalidations", async () => {
48
54
  const requests = [
@@ -59,6 +65,6 @@ describe("MemoryQueue", () => {
59
65
  ];
60
66
  vi.advanceTimersByTime(1);
61
67
  await Promise.all(requests);
62
- expect(globalThis.internalFetch).toHaveBeenCalledTimes(1);
68
+ expect(mockServiceWorkerFetch).toHaveBeenCalledTimes(1);
63
69
  });
64
70
  });
@@ -15,12 +15,13 @@ import { patchFetchCacheSetMissingWaitUntil } from "./patches/plugins/fetch-cach
15
15
  import { inlineFindDir } from "./patches/plugins/find-dir.js";
16
16
  import { patchInstrumentation } from "./patches/plugins/instrumentation.js";
17
17
  import { inlineLoadManifest } from "./patches/plugins/load-manifest.js";
18
+ import { patchNextMinimal } from "./patches/plugins/next-minimal.js";
18
19
  import { handleOptionalDependencies } from "./patches/plugins/optional-deps.js";
19
20
  import { patchDepdDeprecations } from "./patches/plugins/patch-depd-deprecations.js";
20
21
  import { fixRequire } from "./patches/plugins/require.js";
21
22
  import { shimRequireHook } from "./patches/plugins/require-hook.js";
22
23
  import { setWranglerExternal } from "./patches/plugins/wrangler-external.js";
23
- import { normalizePath, patchCodeWithValidations } from "./utils/index.js";
24
+ import { needsExperimentalReact, normalizePath, patchCodeWithValidations } from "./utils/index.js";
24
25
  /** The dist directory of the Cloudflare adapter package */
25
26
  const packageDistDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "../..");
26
27
  /**
@@ -83,6 +84,7 @@ export async function bundleServer(buildOpts) {
83
84
  inlineLoadManifest(updater, buildOpts),
84
85
  inlineBuildId(updater),
85
86
  patchDepdDeprecations(updater),
87
+ patchNextMinimal(updater),
86
88
  // Apply updater updaters, must be the last plugin
87
89
  updater.plugin,
88
90
  ],
@@ -117,7 +119,12 @@ export async function bundleServer(buildOpts) {
117
119
  // We make sure that environment variables that Next.js expects are properly defined
118
120
  "process.env.NEXT_RUNTIME": '"nodejs"',
119
121
  "process.env.NODE_ENV": '"production"',
120
- "process.env.NEXT_MINIMAL": "true",
122
+ // The 2 following defines are used to reduce the bundle size by removing unnecessary code
123
+ // Next uses different precompiled renderers (i.e. `app-page.runtime.prod.js`) based on if you use `TURBOPACK` or some experimental React features
124
+ // Turbopack is not supported for build at the moment, so we disable it
125
+ "process.env.TURBOPACK": "false",
126
+ // This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
127
+ "process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
121
128
  },
122
129
  platform: "node",
123
130
  banner: {
@@ -0,0 +1,7 @@
1
+ import { ContentUpdater } from "./content-updater.js";
2
+ export declare const abortControllerRule = "\nrule:\n all: \n - kind: lexical_declaration\n pattern: let $VAR = new AbortController\n - precedes:\n kind: function_declaration\n stopBy: end\n has:\n kind: statement_block\n has:\n kind: try_statement\n has:\n kind: catch_clause\n has:\n kind: statement_block\n has: \n kind: return_statement\n all: \n - has: \n stopBy: end\n kind: member_expression\n pattern: $VAR.signal.aborted\n - has:\n stopBy: end\n kind: call_expression\n regex: console.error\\(\"Failed to fetch RSC payload for\n \nfix:\n 'let $VAR = {signal:{aborted: false}};'\n";
3
+ export declare const nextMinimalRule = "\nrule:\n kind: member_expression\n pattern: process.env.NEXT_MINIMAL\n any: \n - inside:\n kind: parenthesized_expression\n stopBy: end\n inside:\n kind: if_statement\n any:\n - inside:\n kind: statement_block\n inside:\n kind: method_definition\n any:\n - has: {kind: property_identifier, field: name, regex: runEdgeFunction}\n - has: {kind: property_identifier, field: name, regex: runMiddleware}\n - has: {kind: property_identifier, field: name, regex: imageOptimizer}\n - has:\n kind: statement_block\n has:\n kind: expression_statement\n pattern: res.statusCode = 400;\nfix:\n 'true' \n";
4
+ export declare function patchNextMinimal(updater: ContentUpdater): {
5
+ name: string;
6
+ setup(): void;
7
+ };
@@ -0,0 +1,78 @@
1
+ import { patchCode } from "../ast/util.js";
2
+ // We try to be as specific as possible to avoid patching the wrong thing here
3
+ // It seems that there is a bug in the worker runtime. When the AbortController is created outside of the request context it throws an error (not sure if it's expected or not) except in this case. https://github.com/cloudflare/workerd/issues/3657
4
+ // It fails while requiring the `app-page.runtime.prod.js` file, but instead of throwing an error, it just return an empty object for the `require('app-page.runtime.prod.js')` call which makes every request to an app router page fail.
5
+ // If it's a bug in workerd and it's not expected to throw an error, we can remove this patch.
6
+ export const abortControllerRule = `
7
+ rule:
8
+ all:
9
+ - kind: lexical_declaration
10
+ pattern: let $VAR = new AbortController
11
+ - precedes:
12
+ kind: function_declaration
13
+ stopBy: end
14
+ has:
15
+ kind: statement_block
16
+ has:
17
+ kind: try_statement
18
+ has:
19
+ kind: catch_clause
20
+ has:
21
+ kind: statement_block
22
+ has:
23
+ kind: return_statement
24
+ all:
25
+ - has:
26
+ stopBy: end
27
+ kind: member_expression
28
+ pattern: $VAR.signal.aborted
29
+ - has:
30
+ stopBy: end
31
+ kind: call_expression
32
+ regex: console.error\\("Failed to fetch RSC payload for
33
+
34
+ fix:
35
+ 'let $VAR = {signal:{aborted: false}};'
36
+ `;
37
+ // This rule is used instead of defining `process.env.NEXT_MINIMAL` in the `esbuild config.
38
+ // Do we want to entirely replace these functions to reduce the bundle size?
39
+ // In next `renderHTML` is used as a fallback in case of errors, but in minimal mode it just throws the error and the responsability of handling it is on the infra.
40
+ export const nextMinimalRule = `
41
+ rule:
42
+ kind: member_expression
43
+ pattern: process.env.NEXT_MINIMAL
44
+ any:
45
+ - inside:
46
+ kind: parenthesized_expression
47
+ stopBy: end
48
+ inside:
49
+ kind: if_statement
50
+ any:
51
+ - inside:
52
+ kind: statement_block
53
+ inside:
54
+ kind: method_definition
55
+ any:
56
+ - has: {kind: property_identifier, field: name, regex: runEdgeFunction}
57
+ - has: {kind: property_identifier, field: name, regex: runMiddleware}
58
+ - has: {kind: property_identifier, field: name, regex: imageOptimizer}
59
+ - has:
60
+ kind: statement_block
61
+ has:
62
+ kind: expression_statement
63
+ pattern: res.statusCode = 400;
64
+ fix:
65
+ 'true'
66
+ `;
67
+ export function patchNextMinimal(updater) {
68
+ updater.updateContent("patch-abortController-next15.2", { filter: /app-page(-experimental)?\.runtime\.prod\.js$/, contentFilter: /new AbortController/ }, async ({ contents }) => {
69
+ return patchCode(contents, abortControllerRule);
70
+ });
71
+ updater.updateContent("patch-next-minimal", { filter: /next-server\.(js)$/, contentFilter: /.*/ }, async ({ contents }) => {
72
+ return patchCode(contents, nextMinimalRule);
73
+ });
74
+ return {
75
+ name: "patch-abortController",
76
+ setup() { },
77
+ };
78
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { patchCode } from "../ast/util";
3
+ import { abortControllerRule } from "./next-minimal";
4
+ const appPageRuntimeProdJs = `let p = new AbortController;
5
+ async function h(e3, t3) {
6
+ let { flightRouterState: r3, nextUrl: a2, prefetchKind: i2 } = t3, u2 = { [n2.hY]: "1", [n2.B]: encodeURIComponent(JSON.stringify(r3)) };
7
+ i2 === o.ob.AUTO && (u2[n2._V] = "1"), a2 && (u2[n2.kO] = a2);
8
+ try {
9
+ var c2;
10
+ let t4 = i2 ? i2 === o.ob.TEMPORARY ? "high" : "low" : "auto";
11
+ "export" === process.env.__NEXT_CONFIG_OUTPUT && ((e3 = new URL(e3)).pathname.endsWith("/") ? e3.pathname += "index.txt" : e3.pathname += ".txt");
12
+ let r4 = await m(e3, u2, t4, p.signal), a3 = d(r4.url), h2 = r4.redirected ? a3 : void 0, g = r4.headers.get("content-type") || "", v = !!(null == (c2 = r4.headers.get("vary")) ? void 0 : c2.includes(n2.kO)), b = !!r4.headers.get(n2.jc), S = r4.headers.get(n2.UK), _ = null !== S ? parseInt(S, 10) : -1, w = g.startsWith(n2.al);
13
+ if ("export" !== process.env.__NEXT_CONFIG_OUTPUT || w || (w = g.startsWith("text/plain")), !w || !r4.ok || !r4.body)
14
+ return e3.hash && (a3.hash = e3.hash), f(a3.toString());
15
+ let k = b ? function(e4) {
16
+ let t5 = e4.getReader();
17
+ return new ReadableStream({ async pull(e5) {
18
+ for (; ; ) {
19
+ let { done: r5, value: n3 } = await t5.read();
20
+ if (!r5) {
21
+ e5.enqueue(n3);
22
+ continue;
23
+ }
24
+ return;
25
+ }
26
+ } });
27
+ }(r4.body) : r4.body, E = await y(k);
28
+ if ((0, l.X)() !== E.b)
29
+ return f(r4.url);
30
+ return { flightData: (0, s.aj)(E.f), canonicalUrl: h2, couldBeIntercepted: v, prerendered: E.S, postponed: b, staleTime: _ };
31
+ } catch (t4) {
32
+ return p.signal.aborted || console.error("Failed to fetch RSC payload for " + e3 + ". Falling back to browser navigation.", t4), { flightData: e3.toString(), canonicalUrl: void 0, couldBeIntercepted: false, prerendered: false, postponed: false, staleTime: -1 };
33
+ }
34
+ }
35
+ `;
36
+ describe("Abort controller", () => {
37
+ test("minimal", () => {
38
+ expect(patchCode(appPageRuntimeProdJs, abortControllerRule)).toBe(`let p = {signal:{aborted: false}};
39
+ async function h(e3, t3) {
40
+ let { flightRouterState: r3, nextUrl: a2, prefetchKind: i2 } = t3, u2 = { [n2.hY]: "1", [n2.B]: encodeURIComponent(JSON.stringify(r3)) };
41
+ i2 === o.ob.AUTO && (u2[n2._V] = "1"), a2 && (u2[n2.kO] = a2);
42
+ try {
43
+ var c2;
44
+ let t4 = i2 ? i2 === o.ob.TEMPORARY ? "high" : "low" : "auto";
45
+ "export" === process.env.__NEXT_CONFIG_OUTPUT && ((e3 = new URL(e3)).pathname.endsWith("/") ? e3.pathname += "index.txt" : e3.pathname += ".txt");
46
+ let r4 = await m(e3, u2, t4, p.signal), a3 = d(r4.url), h2 = r4.redirected ? a3 : void 0, g = r4.headers.get("content-type") || "", v = !!(null == (c2 = r4.headers.get("vary")) ? void 0 : c2.includes(n2.kO)), b = !!r4.headers.get(n2.jc), S = r4.headers.get(n2.UK), _ = null !== S ? parseInt(S, 10) : -1, w = g.startsWith(n2.al);
47
+ if ("export" !== process.env.__NEXT_CONFIG_OUTPUT || w || (w = g.startsWith("text/plain")), !w || !r4.ok || !r4.body)
48
+ return e3.hash && (a3.hash = e3.hash), f(a3.toString());
49
+ let k = b ? function(e4) {
50
+ let t5 = e4.getReader();
51
+ return new ReadableStream({ async pull(e5) {
52
+ for (; ; ) {
53
+ let { done: r5, value: n3 } = await t5.read();
54
+ if (!r5) {
55
+ e5.enqueue(n3);
56
+ continue;
57
+ }
58
+ return;
59
+ }
60
+ } });
61
+ }(r4.body) : r4.body, E = await y(k);
62
+ if ((0, l.X)() !== E.b)
63
+ return f(r4.url);
64
+ return { flightData: (0, s.aj)(E.f), canonicalUrl: h2, couldBeIntercepted: v, prerendered: E.S, postponed: b, staleTime: _ };
65
+ } catch (t4) {
66
+ return p.signal.aborted || console.error("Failed to fetch RSC payload for " + e3 + ". Falling back to browser navigation.", t4), { flightData: e3.toString(), canonicalUrl: void 0, couldBeIntercepted: false, prerendered: false, postponed: false, staleTime: -1 };
67
+ }
68
+ }
69
+ `);
70
+ });
71
+ });
@@ -2,4 +2,5 @@ export * from "./apply-patches.js";
2
2
  export * from "./create-config-files.js";
3
3
  export * from "./ensure-cf-config.js";
4
4
  export * from "./extract-project-env-vars.js";
5
+ export * from "./needs-experimental-react.js";
5
6
  export * from "./normalize-path.js";
@@ -2,4 +2,5 @@ export * from "./apply-patches.js";
2
2
  export * from "./create-config-files.js";
3
3
  export * from "./ensure-cf-config.js";
4
4
  export * from "./extract-project-env-vars.js";
5
+ export * from "./needs-experimental-react.js";
5
6
  export * from "./normalize-path.js";
@@ -0,0 +1,11 @@
1
+ import type { NextConfig } from "@opennextjs/aws/types/next-types";
2
+ interface ExtendedNextConfig extends NextConfig {
3
+ experimental: {
4
+ ppr?: boolean;
5
+ taint?: boolean;
6
+ viewTransition?: boolean;
7
+ serverActions?: boolean;
8
+ };
9
+ }
10
+ export declare function needsExperimentalReact(nextConfig: ExtendedNextConfig): boolean;
11
+ export {};
@@ -0,0 +1,5 @@
1
+ // Copied from https://github.com/vercel/next.js/blob/4518bc91641a0fd938664b781e12ae7c145f3396/packages/next/src/lib/needs-experimental-react.ts#L3-L6
2
+ export function needsExperimentalReact(nextConfig) {
3
+ const { ppr, taint, viewTransition } = nextConfig.experimental || {};
4
+ return Boolean(ppr || taint || viewTransition);
5
+ }
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.5.8",
4
+ "version": "0.5.10",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "opennextjs-cloudflare": "dist/cli/index.js"