From 8de2c0422e3d93848371cf07816ccb989070cd62 Mon Sep 17 00:00:00 2001
From: Dario Piotrowicz <dario@cloudflare.com>
Date: Mon, 27 Jan 2025 10:30:02 +0000
Subject: [PATCH] introduce new `initOpenNextCloudflareForDev` utility and make
 `getCloudflareContext` synchronous (#265)

Co-authored-by: Victor Berchet <victor@suumit.com>
---
 .changeset/chilly-dryers-begin.md             |  28 +++
 examples/api/app/api/hello/route.ts           |   5 +-
 examples/api/e2e/playwright.config.ts         |   4 +-
 examples/api/e2e/playwright.dev.config.ts     |   3 +-
 examples/api/next.config.mjs                  |   4 +
 .../create-next-app/e2e/playwright.config.ts  |   4 +-
 examples/create-next-app/next.config.mjs      |   4 +
 examples/middleware/app/middleware/page.tsx   |  24 ++-
 .../middleware/e2e/cloudflare-context.spec.ts |   9 +
 examples/middleware/e2e/playwright.config.ts  |   4 +-
 .../middleware/e2e/playwright.dev.config.ts   |  54 ++++++
 examples/middleware/middleware.ts             |  16 +-
 examples/middleware/next.config.mjs           |   4 +
 examples/middleware/package.json              |   3 +-
 examples/middleware/wrangler.json             |   6 +-
 package.json                                  |   1 +
 packages/cloudflare/README.md                 |  67 +------
 .../cloudflare/src/api/cloudflare-context.ts  | 164 ++++++++++++++++++
 .../src/api/get-cloudflare-context.ts         |  86 ---------
 packages/cloudflare/src/api/index.ts          |   2 +-
 packages/cloudflare/src/api/kvCache.ts        |  51 +++---
 pnpm-lock.yaml                                |  22 +--
 pnpm-workspace.yaml                           |   2 +-
 23 files changed, 361 insertions(+), 206 deletions(-)
 create mode 100644 .changeset/chilly-dryers-begin.md
 create mode 100644 examples/middleware/e2e/cloudflare-context.spec.ts
 create mode 100644 examples/middleware/e2e/playwright.dev.config.ts
 create mode 100644 packages/cloudflare/src/api/cloudflare-context.ts
 delete mode 100644 packages/cloudflare/src/api/get-cloudflare-context.ts

diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md
new file mode 100644
index 00000000..40ce3f52
--- /dev/null
+++ b/.changeset/chilly-dryers-begin.md
@@ -0,0 +1,28 @@
+---
+"@opennextjs/cloudflare": minor
+---
+
+introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareContext` synchronous
+
+this change introduces a new `initOpenNextCloudflareForDev` function that must called in the [Next.js config file](https://nextjs.org/docs/app/api-reference/config/next-config-js) to integrate the Next.js dev server with the open-next Cloudflare adapter.
+
+Also makes `getCloudflareContext` synchronous.
+
+Additionally the `getCloudflareContext` can now work during local development (`next dev`) in the edge runtime (including middlewares).
+
+Moving forward we'll recommend that all applications include the use of the `initOpenNextCloudflareForDev` utility in their config file (there is no downside in doing so and it only effect local development).
+
+Example:
+
+```js
+// next.config.mjs
+
+import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
+
+initOpenNextCloudflareForDev();
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
+```
diff --git a/examples/api/app/api/hello/route.ts b/examples/api/app/api/hello/route.ts
index c42676cc..79a62065 100644
--- a/examples/api/app/api/hello/route.ts
+++ b/examples/api/app/api/hello/route.ts
@@ -11,9 +11,8 @@ export async function GET() {
     return new Response("Hello World!");
   }
 
-  // Retrieve the bindings defined in wrangler.toml
-  const { env } = await getCloudflareContext();
-  return new Response(env.hello);
+  // Retrieve the bindings defined in wrangler.json
+  return new Response(getCloudflareContext().env.hello);
 }
 
 export async function POST(request: Request) {
diff --git a/examples/api/e2e/playwright.config.ts b/examples/api/e2e/playwright.config.ts
index 4bca883e..0f5f526d 100644
--- a/examples/api/e2e/playwright.config.ts
+++ b/examples/api/e2e/playwright.config.ts
@@ -1,6 +1,7 @@
 import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
 
-declare var process: { env: Record<string, string> };
+declare const process: typeof nodeProcess;
 
 /**
  * See https://playwright.dev/docs/test-configuration.
@@ -49,5 +50,6 @@ export default defineConfig({
     command: "pnpm preview:worker",
     url: "http://localhost:8770",
     reuseExistingServer: !process.env.CI,
+    timeout: 70_000,
   },
 });
diff --git a/examples/api/e2e/playwright.dev.config.ts b/examples/api/e2e/playwright.dev.config.ts
index d3d61c03..b14e729e 100644
--- a/examples/api/e2e/playwright.dev.config.ts
+++ b/examples/api/e2e/playwright.dev.config.ts
@@ -1,6 +1,7 @@
 import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
 
-declare var process: { env: Record<string, string> };
+declare const process: typeof nodeProcess;
 
 /**
  * See https://playwright.dev/docs/test-configuration.
diff --git a/examples/api/next.config.mjs b/examples/api/next.config.mjs
index 4678774e..24073f0a 100644
--- a/examples/api/next.config.mjs
+++ b/examples/api/next.config.mjs
@@ -1,3 +1,7 @@
+import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
+
+initOpenNextCloudflareForDev();
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {};
 
diff --git a/examples/create-next-app/e2e/playwright.config.ts b/examples/create-next-app/e2e/playwright.config.ts
index 6aad04dd..083a501c 100644
--- a/examples/create-next-app/e2e/playwright.config.ts
+++ b/examples/create-next-app/e2e/playwright.config.ts
@@ -1,6 +1,7 @@
 import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
 
-declare const process: { env: Record<string, string> };
+declare const process: typeof nodeProcess;
 
 /**
  * See https://playwright.dev/docs/test-configuration.
@@ -49,5 +50,6 @@ export default defineConfig({
     command: "pnpm preview:worker",
     url: "http://localhost:8771",
     reuseExistingServer: !process.env.CI,
+    timeout: 70_000,
   },
 });
diff --git a/examples/create-next-app/next.config.mjs b/examples/create-next-app/next.config.mjs
index 4678774e..24073f0a 100644
--- a/examples/create-next-app/next.config.mjs
+++ b/examples/create-next-app/next.config.mjs
@@ -1,3 +1,7 @@
+import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
+
+initOpenNextCloudflareForDev();
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {};
 
diff --git a/examples/middleware/app/middleware/page.tsx b/examples/middleware/app/middleware/page.tsx
index aa28fa5f..51739a0b 100644
--- a/examples/middleware/app/middleware/page.tsx
+++ b/examples/middleware/app/middleware/page.tsx
@@ -1,3 +1,25 @@
+import { headers } from "next/headers";
+
 export default function MiddlewarePage() {
-  return <h1>Via middleware</h1>;
+  const cloudflareContextHeader = headers().get("x-cloudflare-context");
+
+  return (
+    <>
+      <h1>Via middleware</h1>
+      <p>
+        The value of the <i>x-cloudflare-context</i> header is: <br />
+        <span
+          style={{
+            display: "inline-block",
+            margin: "1rem 2rem",
+            color: "grey",
+            fontSize: "1.2rem",
+          }}
+          data-testid="cloudflare-context-header"
+        >
+          {cloudflareContextHeader}
+        </span>
+      </p>
+    </>
+  );
 }
diff --git a/examples/middleware/e2e/cloudflare-context.spec.ts b/examples/middleware/e2e/cloudflare-context.spec.ts
new file mode 100644
index 00000000..ed27b846
--- /dev/null
+++ b/examples/middleware/e2e/cloudflare-context.spec.ts
@@ -0,0 +1,9 @@
+import { test, expect } from "@playwright/test";
+
+test("middlewares have access to the cloudflare context", async ({ page }) => {
+  await page.goto("/middleware");
+  const cloudflareContextHeaderElement = page.getByTestId("cloudflare-context-header");
+  expect(await cloudflareContextHeaderElement.textContent()).toContain(
+    "typeof `cloudflareContext.env` = object"
+  );
+});
diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts
index d6d8499c..66c22aa5 100644
--- a/examples/middleware/e2e/playwright.config.ts
+++ b/examples/middleware/e2e/playwright.config.ts
@@ -1,6 +1,7 @@
 import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
 
-declare const process: { env: Record<string, string> };
+declare const process: typeof nodeProcess;
 
 /**
  * See https://playwright.dev/docs/test-configuration.
@@ -49,5 +50,6 @@ export default defineConfig({
     command: "pnpm preview:worker",
     url: "http://localhost:8774",
     reuseExistingServer: !process.env.CI,
+    timeout: 70_000,
   },
 });
diff --git a/examples/middleware/e2e/playwright.dev.config.ts b/examples/middleware/e2e/playwright.dev.config.ts
new file mode 100644
index 00000000..6fabd3da
--- /dev/null
+++ b/examples/middleware/e2e/playwright.dev.config.ts
@@ -0,0 +1,54 @@
+import { defineConfig, devices } from "@playwright/test";
+import type nodeProcess from "node:process";
+
+declare const process: typeof nodeProcess;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig({
+  testDir: "./",
+  /* Run tests in files in parallel */
+  fullyParallel: true,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!process.env.CI,
+  /* Retry on CI only */
+  retries: process.env.CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: process.env.CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: "html",
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: "http://localhost:3334",
+
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: "on-first-retry",
+  },
+
+  /* Configure projects for major browsers */
+  projects: [
+    {
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
+    },
+
+    {
+      name: "firefox",
+      use: { ...devices["Desktop Firefox"] },
+    },
+
+    {
+      name: "webkit",
+      use: { ...devices["Desktop Safari"] },
+    },
+  ],
+
+  /* Run your local dev server before starting the tests */
+  webServer: {
+    command: "pnpm dev --port 3334",
+    url: "http://localhost:3334",
+    reuseExistingServer: !process.env.CI,
+  },
+});
diff --git a/examples/middleware/middleware.ts b/examples/middleware/middleware.ts
index 9145eb73..d7facf06 100644
--- a/examples/middleware/middleware.ts
+++ b/examples/middleware/middleware.ts
@@ -1,6 +1,8 @@
 import { NextRequest, NextResponse, NextFetchEvent } from "next/server";
 import { clerkMiddleware } from "@clerk/nextjs/server";
 
+import { getCloudflareContext } from "@opennextjs/cloudflare";
+
 export function middleware(request: NextRequest, event: NextFetchEvent) {
   console.log("middleware");
   if (request.nextUrl.pathname === "/about") {
@@ -16,7 +18,19 @@ export function middleware(request: NextRequest, event: NextFetchEvent) {
     })(request, event);
   }
 
-  return NextResponse.next();
+  const requestHeaders = new Headers(request.headers);
+  const cloudflareContext = getCloudflareContext();
+
+  requestHeaders.set(
+    "x-cloudflare-context",
+    `typeof \`cloudflareContext.env\` = ${typeof cloudflareContext.env}`
+  );
+
+  return NextResponse.next({
+    request: {
+      headers: requestHeaders,
+    },
+  });
 }
 
 export const config = {
diff --git a/examples/middleware/next.config.mjs b/examples/middleware/next.config.mjs
index 4678774e..24073f0a 100644
--- a/examples/middleware/next.config.mjs
+++ b/examples/middleware/next.config.mjs
@@ -1,3 +1,7 @@
+import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
+
+initOpenNextCloudflareForDev();
+
 /** @type {import('next').NextConfig} */
 const nextConfig = {};
 
diff --git a/examples/middleware/package.json b/examples/middleware/package.json
index e906c090..4b5dc8de 100644
--- a/examples/middleware/package.json
+++ b/examples/middleware/package.json
@@ -9,7 +9,8 @@
     "build:worker": "pnpm opennextjs-cloudflare",
     "dev:worker": "wrangler dev --port 8774 --inspector-port 9334",
     "preview:worker": "pnpm build:worker && pnpm dev:worker",
-    "e2e": "playwright test -c e2e/playwright.config.ts"
+    "e2e": "playwright test -c e2e/playwright.config.ts",
+    "e2e:dev": "playwright test -c e2e/playwright.dev.config.ts"
   },
   "dependencies": {
     "@clerk/nextjs": "6.9.6",
diff --git a/examples/middleware/wrangler.json b/examples/middleware/wrangler.json
index b9cced50..d4a80fd0 100644
--- a/examples/middleware/wrangler.json
+++ b/examples/middleware/wrangler.json
@@ -7,5 +7,9 @@
   "assets": {
     "directory": ".open-next/assets",
     "binding": "ASSETS"
-  }
+  },
+  "vars": {
+    "MY_VAR": "my-var"
+  },
+  "kv_namespaces": [{ "binding": "MY_KV", "id": "<id>" }]
 }
diff --git a/package.json b/package.json
index 759db9d8..66c00555 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
     "prettier:fix": "prettier --write .",
     "lint:check": "pnpm -r lint:check",
     "lint:fix": "pnpm -r lint:fix",
+    "fix": "pnpm prettier:fix && pnpm lint:fix",
     "ts:check": "pnpm -r ts:check",
     "test": "pnpm -r test",
     "code:checks": "pnpm prettier:check && pnpm lint:check && pnpm ts:check",
diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md
index fe083089..bfc0336a 100644
--- a/packages/cloudflare/README.md
+++ b/packages/cloudflare/README.md
@@ -6,72 +6,7 @@ Deploy Next.js apps to Cloudflare!
 
 ## Get started
 
-You can use [`create-next-app`](https://nextjs.org/docs/pages/api-reference/cli/create-next-app) to start a new application or take an existing Next.js application and deploy it to Cloudflare using the following few steps:
-
-## Configure your app
-
-- add the following `devDependencies` to the `package.json`:
-
-  ```bash
-  npm add -D wrangler@latest @opennextjs/cloudflare
-  # or
-  pnpm add -D wrangler@latest @opennextjs/cloudflare
-  # or
-  yarn add -D wrangler@latest @opennextjs/cloudflare
-  # or
-  bun add -D wrangler@latest @opennextjs/cloudflare
-  ```
-
-- add a `wrangler.json` at the root of your project
-
-```json
-{
-  "$schema": "node_modules/wrangler/config-schema.json",
-  "main": ".open-next/worker.js",
-  "name": "<your-app-name>",
-  "compatibility_date": "2024-12-30",
-  "compatibility_flags": ["nodejs_compat"],
-  "assets": {
-    "directory": ".open-next/assets",
-    "binding": "ASSETS"
-  }
-}
-```
-
-- add a `open-next.config.ts` at the root of your project:
-
-```ts
-import type { OpenNextConfig } from "open-next/types/open-next";
-
-const config: OpenNextConfig = {
-  default: {
-    override: {
-      wrapper: "cloudflare-node",
-      converter: "edge",
-      // Unused implementation
-      incrementalCache: "dummy",
-      tagCache: "dummy",
-      queue: "dummy",
-    },
-  },
-
-  middleware: {
-    external: true,
-    override: {
-      wrapper: "cloudflare-edge",
-      converter: "edge",
-      proxyExternalRequest: "fetch",
-    },
-  },
-};
-
-export default config;
-```
-
-## Known issues
-
-- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored
-- Maybe more, still experimental...
+To get started with the adapter visit the [official get started documentation](https://opennext.js.org/cloudflare/get-started).
 
 ## Local development
 
diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts
new file mode 100644
index 00000000..e9c39edf
--- /dev/null
+++ b/packages/cloudflare/src/api/cloudflare-context.ts
@@ -0,0 +1,164 @@
+import type { Context, RunningCodeOptions } from "node:vm";
+
+declare global {
+  interface CloudflareEnv {
+    NEXT_CACHE_WORKERS_KV?: KVNamespace;
+    ASSETS?: Fetcher;
+  }
+}
+
+export type CloudflareContext<
+  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
+  Context = ExecutionContext,
+> = {
+  /**
+   * the worker's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
+   */
+  env: CloudflareEnv;
+  /**
+   * the request's [cf properties](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties)
+   */
+  cf: CfProperties | undefined;
+  /**
+   * the current [execution context](https://developers.cloudflare.com/workers/runtime-apis/context)
+   */
+  ctx: Context;
+};
+
+/**
+ * Symbol used as an index in the global scope to set and retrieve the Cloudflare context
+ *
+ * This is used both in production (in the actual built worker) and in development (`next dev`)
+ *
+ * Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
+ */
+const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
+
+/**
+ * `globalThis` override for internal usage (simply the standard `globalThis`) enhanced with
+ * a property indexed by the `cloudflareContextSymbol`
+ */
+type InternalGlobalThis<
+  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
+  Context = ExecutionContext,
+> = typeof globalThis & {
+  [cloudflareContextSymbol]: CloudflareContext<CfProperties, Context> | undefined;
+};
+
+/**
+ * Utility to get the current Cloudflare context
+ *
+ * @returns the cloudflare context
+ */
+export function getCloudflareContext<
+  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
+  Context = ExecutionContext,
+>(): CloudflareContext<CfProperties, Context> {
+  const global = globalThis as InternalGlobalThis<CfProperties, Context>;
+
+  const cloudflareContext = global[cloudflareContextSymbol];
+
+  if (!cloudflareContext) {
+    // the cloudflare context is initialized by the worker and is always present in production/preview
+    // during local development (`next dev`) it might be missing only if the developers hasn't called
+    // the `initOpenNextCloudflareForDev` function in their Next.js config file
+    const getContextFunctionName = getCloudflareContext.name;
+    const initFunctionName = initOpenNextCloudflareForDev.name;
+    throw new Error(
+      `\n\n\`${getContextFunctionName}\` has been called during development without having called` +
+        ` the \`${initFunctionName}\` function inside the Next.js config file.\n\n` +
+        `In order to use \`${getContextFunctionName}\` import and call ${initFunctionName} in the Next.js config file.\n\n` +
+        "Example: \n   ```\n   // next.config.mjs\n\n" +
+        `   import { ${initFunctionName} } from "@opennextjs/cloudflare";\n\n` +
+        `   ${initFunctionName}();\n\n` +
+        "   /** @type {import('next').NextConfig} */\n" +
+        "   const nextConfig = {};\n" +
+        "   export default nextConfig;\n" +
+        "   ```\n" +
+        "\n(note: currently middlewares in Next.js are always run using the edge runtime)\n\n"
+    );
+  }
+
+  return cloudflareContext;
+}
+
+/**
+ * Performs some initial setup to integrate as best as possible the local Next.js dev server (run via `next dev`)
+ * with the open-next Cloudflare adapter
+ *
+ * Note: this function should only be called inside the Next.js config file, and although async it doesn't need to be `await`ed
+ */
+export async function initOpenNextCloudflareForDev() {
+  const context = await getCloudflareContextFromWrangler();
+
+  addCloudflareContextToNodejsGlobal(context);
+
+  await monkeyPatchVmModuleEdgeContext(context);
+}
+
+/**
+ * Adds the cloudflare context to the global scope in which the Next.js dev node.js process runs in, enabling
+ * future calls to `getCloudflareContext` to retrieve and return such context
+ *
+ * @param cloudflareContext the cloudflare context to add to the node.sj global scope
+ */
+function addCloudflareContextToNodejsGlobal(cloudflareContext: CloudflareContext<CfProperties, Context>) {
+  const global = globalThis as InternalGlobalThis<CfProperties, Context>;
+  global[cloudflareContextSymbol] = cloudflareContext;
+}
+
+/**
+ * Next.js uses the Node.js vm module's `runInContext()` function to evaluate edge functions
+ * in a runtime context that tries to simulate as accurately as possible the actual production runtime
+ * behavior, see: https://github.com/vercel/next.js/blob/9a1cd3/packages/next/src/server/web/sandbox/context.ts#L525-L527
+ *
+ * This function monkey-patches the Node.js `vm` module to override the `runInContext()` function so that the
+ * cloudflare context is added to the runtime context's global scope before edge functions are evaluated
+ *
+ * @param cloudflareContext the cloudflare context to patch onto the "edge" runtime context global scope
+ */
+async function monkeyPatchVmModuleEdgeContext(cloudflareContext: CloudflareContext<CfProperties, Context>) {
+  const require = (
+    await import(/* webpackIgnore: true */ `${"__module".replaceAll("_", "")}`)
+  ).default.createRequire(import.meta.url);
+
+  // eslint-disable-next-line unicorn/prefer-node-protocol -- the `next dev` compiler doesn't accept the node prefix
+  const vmModule = require("vm");
+
+  const originalRunInContext = vmModule.runInContext.bind(vmModule);
+
+  vmModule.runInContext = (
+    code: string,
+    contextifiedObject: Context,
+    options?: RunningCodeOptions | string
+  ) => {
+    type RuntimeContext = Record<string, unknown> & {
+      [cloudflareContextSymbol]?: CloudflareContext<CfProperties, Context>;
+    };
+    const runtimeContext = contextifiedObject as RuntimeContext;
+    runtimeContext[cloudflareContextSymbol] ??= cloudflareContext;
+    return originalRunInContext(code, contextifiedObject, options);
+  };
+}
+
+/**
+ * Gets a cloudflare context object from wrangler
+ *
+ * @returns the cloudflare context ready for use
+ */
+async function getCloudflareContextFromWrangler<
+  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
+  Context = ExecutionContext,
+>(): Promise<CloudflareContext<CfProperties, Context>> {
+  // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
+  const { getPlatformProxy } = await import(/* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`);
+  const { env, cf, ctx } = await getPlatformProxy({
+    // This allows the selection of a wrangler environment while running in next dev mode
+    environment: process.env.NEXT_DEV_WRANGLER_ENV,
+  });
+  return {
+    env,
+    cf: cf as unknown as CfProperties,
+    ctx: ctx as Context,
+  };
+}
diff --git a/packages/cloudflare/src/api/get-cloudflare-context.ts b/packages/cloudflare/src/api/get-cloudflare-context.ts
deleted file mode 100644
index 1e627309..00000000
--- a/packages/cloudflare/src/api/get-cloudflare-context.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-declare global {
-  interface CloudflareEnv {
-    NEXT_CACHE_WORKERS_KV?: KVNamespace;
-    ASSETS?: Fetcher;
-  }
-}
-
-export type CloudflareContext<
-  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
-  Context = ExecutionContext,
-> = {
-  /**
-   * the worker's [bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings/)
-   */
-  env: CloudflareEnv;
-  /**
-   * the request's [cf properties](https://developers.cloudflare.com/workers/runtime-apis/request/#the-cf-property-requestinitcfproperties)
-   */
-  cf: CfProperties | undefined;
-  /**
-   * the current [execution context](https://developers.cloudflare.com/workers/runtime-apis/context)
-   */
-  ctx: Context;
-};
-
-// Note: this symbol needs to be kept in sync with the one used in `src/cli/templates/worker.ts`
-const cloudflareContextSymbol = Symbol.for("__cloudflare-context__");
-
-/**
- * Utility to get the current Cloudflare context
- *
- * @returns the cloudflare context
- */
-export async function getCloudflareContext<
-  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
-  Context = ExecutionContext,
->(): Promise<CloudflareContext<CfProperties, Context>> {
-  const global = globalThis as unknown as {
-    [cloudflareContextSymbol]: CloudflareContext<CfProperties, Context> | undefined;
-  };
-
-  const cloudflareContext = global[cloudflareContextSymbol];
-
-  if (!cloudflareContext) {
-    // the cloudflare context is initialized by the worker and is always present in production/preview,
-    // so, it not being present means that the application is running under `next dev`
-    return getCloudflareContextInNextDev();
-  }
-
-  return cloudflareContext;
-}
-
-const cloudflareContextInNextDevSymbol = Symbol.for("__next-dev/cloudflare-context__");
-
-/**
- * Gets a local proxy version of the cloudflare context (created using `getPlatformProxy`) when
- * running in the standard next dev server (via `next dev`)
- *
- * @returns the local proxy version of the cloudflare context
- */
-async function getCloudflareContextInNextDev<
-  CfProperties extends Record<string, unknown> = IncomingRequestCfProperties,
-  Context = ExecutionContext,
->(): Promise<CloudflareContext<CfProperties, Context>> {
-  const global = globalThis as unknown as {
-    [cloudflareContextInNextDevSymbol]: CloudflareContext<CfProperties, Context> | undefined;
-  };
-
-  if (!global[cloudflareContextInNextDevSymbol]) {
-    // Note: we never want wrangler to be bundled in the Next.js app, that's why the import below looks like it does
-    const { getPlatformProxy } = await import(
-      /* webpackIgnore: true */ `${"__wrangler".replaceAll("_", "")}`
-    );
-    const { env, cf, ctx } = await getPlatformProxy({
-      // This allows the selection of a wrangler environment while running in next dev mode
-      environment: process.env.NEXT_DEV_WRANGLER_ENV,
-    });
-    global[cloudflareContextInNextDevSymbol] = {
-      env,
-      cf: cf as unknown as CfProperties,
-      ctx: ctx as Context,
-    };
-  }
-
-  return global[cloudflareContextInNextDevSymbol]!;
-}
diff --git a/packages/cloudflare/src/api/index.ts b/packages/cloudflare/src/api/index.ts
index 5442c415..574ce7de 100644
--- a/packages/cloudflare/src/api/index.ts
+++ b/packages/cloudflare/src/api/index.ts
@@ -1 +1 @@
-export * from "./get-cloudflare-context.js";
+export * from "./cloudflare-context.js";
diff --git a/packages/cloudflare/src/api/kvCache.ts b/packages/cloudflare/src/api/kvCache.ts
index 6e780a9b..e7203a05 100644
--- a/packages/cloudflare/src/api/kvCache.ts
+++ b/packages/cloudflare/src/api/kvCache.ts
@@ -1,8 +1,7 @@
-import type { KVNamespace } from "@cloudflare/workers-types";
 import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
 import { IgnorableError, RecoverableError } from "@opennextjs/aws/utils/error.js";
 
-import { getCloudflareContext } from "./get-cloudflare-context.js";
+import { getCloudflareContext } from "./cloudflare-context.js";
 
 export const CACHE_ASSET_DIR = "cnd-cgi/_next_cache";
 
@@ -17,19 +16,16 @@ export const STATUS_DELETED = 1;
  */
 class Cache implements IncrementalCache {
   readonly name = "cloudflare-kv";
-  protected initialized = false;
-  protected kv: KVNamespace | undefined;
-  protected assets: Fetcher | undefined;
 
   async get<IsFetch extends boolean = false>(
     key: string,
     isFetch?: IsFetch
   ): Promise<WithLastModified<CacheValue<IsFetch>>> {
-    if (!this.initialized) {
-      await this.init();
-    }
+    const cfEnv = getCloudflareContext().env;
+    const kv = cfEnv.NEXT_CACHE_WORKERS_KV;
+    const assets = cfEnv.ASSETS;
 
-    if (!(this.kv || this.assets)) {
+    if (!(kv || assets)) {
       throw new IgnorableError(`No KVNamespace nor Fetcher`);
     }
 
@@ -42,19 +38,19 @@ class Cache implements IncrementalCache {
         status?: number;
       } | null = null;
 
-      if (this.kv) {
+      if (kv) {
         this.debug(`- From KV`);
         const kvKey = this.getKVKey(key, isFetch);
-        entry = await this.kv.get(kvKey, "json");
+        entry = await kv.get(kvKey, "json");
         if (entry?.status === STATUS_DELETED) {
           return {};
         }
       }
 
-      if (!entry && this.assets) {
+      if (!entry && assets) {
         this.debug(`- From Assets`);
         const url = this.getAssetUrl(key, isFetch);
-        const response = await this.assets.fetch(url);
+        const response = await assets.fetch(url);
         if (response.ok) {
           // TODO: consider populating KV with the asset value if faster.
           // This could be optional as KV writes are $$.
@@ -78,19 +74,20 @@ class Cache implements IncrementalCache {
     value: CacheValue<IsFetch>,
     isFetch?: IsFetch
   ): Promise<void> {
-    if (!this.initialized) {
-      await this.init();
-    }
-    if (!this.kv) {
+    const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
+
+    if (!kv) {
       throw new IgnorableError(`No KVNamespace`);
     }
+
     this.debug(`Set ${key}`);
+
     try {
       const kvKey = this.getKVKey(key, isFetch);
       // Note: We can not set a TTL as we might fallback to assets,
       //       still removing old data (old BUILD_ID) could help avoiding
       //       the cache growing too big.
-      await this.kv.put(
+      await kv.put(
         kvKey,
         JSON.stringify({
           value,
@@ -105,17 +102,18 @@ class Cache implements IncrementalCache {
   }
 
   async delete(key: string): Promise<void> {
-    if (!this.initialized) {
-      await this.init();
-    }
-    if (!this.kv) {
+    const kv = getCloudflareContext().env.NEXT_CACHE_WORKERS_KV;
+
+    if (!kv) {
       throw new IgnorableError(`No KVNamespace`);
     }
+
     this.debug(`Delete ${key}`);
+
     try {
       const kvKey = this.getKVKey(key, /* isFetch= */ false);
       // Do not delete the key as we would then fallback to the assets.
-      await this.kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
+      await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
     } catch {
       throw new RecoverableError(`Failed to delete cache [${key}]`);
     }
@@ -140,13 +138,6 @@ class Cache implements IncrementalCache {
   protected getBuildId() {
     return process.env.NEXT_BUILD_ID ?? "no-build-id";
   }
-
-  protected async init() {
-    const env = (await getCloudflareContext()).env;
-    this.kv = env.NEXT_CACHE_WORKERS_KV;
-    this.assets = env.ASSETS;
-    this.initialized = true;
-  }
 }
 
 export default new Cache();
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2068aee2..eac18364 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -85,8 +85,8 @@ catalogs:
       specifier: ^2.1.1
       version: 2.1.1
     wrangler:
-      specifier: ^3.103.0
-      version: 3.104.0
+      specifier: ^3.105.0
+      version: 3.105.0
   e2e:
     '@types/node':
       specifier: 20.17.6
@@ -171,7 +171,7 @@ importers:
         version: 22.2.0
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
 
   examples/create-next-app:
     dependencies:
@@ -217,7 +217,7 @@ importers:
         version: 5.7.3
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
 
   examples/e2e/app-router:
     dependencies:
@@ -316,7 +316,7 @@ importers:
         version: 5.7.3
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
 
   examples/vercel-blog-starter:
     dependencies:
@@ -371,7 +371,7 @@ importers:
         version: 5.7.3
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
 
   examples/vercel-commerce:
     dependencies:
@@ -438,7 +438,7 @@ importers:
         version: 5.7.3
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
 
   packages/cloudflare:
     dependencies:
@@ -462,7 +462,7 @@ importers:
         version: 23.0.0
       wrangler:
         specifier: 'catalog:'
-        version: 3.104.0(@cloudflare/workers-types@4.20250109.0)
+        version: 3.105.0(@cloudflare/workers-types@4.20250109.0)
       yaml:
         specifier: ^2.7.0
         version: 2.7.0
@@ -6085,8 +6085,8 @@ packages:
     engines: {node: '>=16'}
     hasBin: true
 
-  wrangler@3.104.0:
-    resolution: {integrity: sha512-txxgkKZwPQrX1PDgY+ATWnnx4GSeNxUrnBumudWPRmXG0JdLzCf09R+723slMMT1m+CKQXU1KvuUHc/GxTnTyA==}
+  wrangler@3.105.0:
+    resolution: {integrity: sha512-NX10iuUXtgiVRG9YJ7dwwEUuhQ38hu4stcxMWq4dbKCnfcOj7fLFh+HwaWudqOr1jDCPrnSOQVkgfAfG3ZH9Lw==}
     engines: {node: '>=16.17.0'}
     hasBin: true
     peerDependencies:
@@ -13347,7 +13347,7 @@ snapshots:
       '@cloudflare/workerd-linux-arm64': 1.20241230.0
       '@cloudflare/workerd-windows-64': 1.20241230.0
 
-  wrangler@3.104.0(@cloudflare/workers-types@4.20250109.0):
+  wrangler@3.105.0(@cloudflare/workers-types@4.20250109.0):
     dependencies:
       '@cloudflare/kv-asset-handler': 0.3.4
       '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index b406d915..4c4b5b30 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -33,7 +33,7 @@ catalog:
   typescript-eslint: ^8.7.0
   typescript: ^5.7.3
   vitest: ^2.1.1
-  wrangler: ^3.103.0
+  wrangler: ^3.105.0
 
 # e2e tests
 catalogs: