From 60a62968a5ab6ba2e598aa299dc159dc64cfed6e Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 17 Jan 2025 14:56:22 +0000 Subject: [PATCH 01/20] introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareContext` synchronous --- .changeset/chilly-dryers-begin.md | 47 +++++ examples/api/e2e/playwright.config.ts | 3 + examples/api/next.config.mjs | 4 + .../create-next-app/e2e/playwright.config.ts | 3 + examples/create-next-app/next.config.mjs | 4 + examples/middleware/app/middleware/page.tsx | 24 ++- .../middleware/e2e/cloudflare-context.spec.ts | 14 ++ examples/middleware/e2e/playwright.config.ts | 3 + .../middleware/e2e/playwright.dev.config.ts | 53 ++++++ examples/middleware/middleware.ts | 18 +- examples/middleware/next.config.mjs | 4 + examples/middleware/package.json | 3 +- examples/middleware/wrangler.json | 6 +- .../cloudflare/src/api/cloudflare-context.ts | 175 ++++++++++++++++++ .../src/api/get-cloudflare-context.ts | 86 --------- packages/cloudflare/src/api/index.ts | 2 +- packages/cloudflare/src/api/kvCache.ts | 2 +- pnpm-lock.yaml | 22 +-- pnpm-workspace.yaml | 2 +- 19 files changed, 370 insertions(+), 105 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..3824ebc0 --- /dev/null +++ b/.changeset/chilly-dryers-begin.md @@ -0,0 +1,47 @@ +--- +"@opennextjs/cloudflare": patch +--- + +introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareContext` synchronous + +introduce a new `initOpenNextCloudflareForDev` function that when called in the [Next.js config file](https://nextjs.org/docs/app/api-reference/config/next-config-js) integrates the Next.js dev server with the open-next Cloudflare adapter. Most noticeably this enables `getCloudflareContext` to work in +the edge runtime (including middlewares) during development. + +Moving forward we'll recommend that all applications include the use of this 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"; + +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +initOpenNextCloudflareForDev(); + +export default nextConfig; +``` + +thanks to this new utility we are also able to make `getCloudflareContext` synchronous, so `await`ing such function is no longer necessary + +Example: + +- Before: + + ```js + import { getCloudflareContext } from "@opennextjs/cloudflare"; + + const cloudflareContext = await getCloudflareContext(); + // cloudflareContext use + ``` + +- After: + + ```js + import { getCloudflareContext } from "@opennextjs/cloudflare"; + + const cloudflareContext = getCloudflareContext(); + // cloudflareContext use + ``` diff --git a/examples/api/e2e/playwright.config.ts b/examples/api/e2e/playwright.config.ts index 4bca883e..3800628b 100644 --- a/examples/api/e2e/playwright.config.ts +++ b/examples/api/e2e/playwright.config.ts @@ -49,5 +49,8 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8770", reuseExistingServer: !process.env.CI, + // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, + // that's why we need a longer timeout here (we just add 10 seconds to the default 60) + timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, }, }); 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..e44a9e0a 100644 --- a/examples/create-next-app/e2e/playwright.config.ts +++ b/examples/create-next-app/e2e/playwright.config.ts @@ -49,5 +49,8 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8771", reuseExistingServer: !process.env.CI, + // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, + // that's why we need a longer timeout here (we just add 10 seconds to the default 60) + timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, }, }); 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

Via middleware

; + const cloudflareContextHeader = headers().get("x-cloudflare-context"); + + return ( + <> +

Via middleware

+

+ The value of the x-cloudflare-context header is:
+ + {cloudflareContextHeader} + +

+ + ); } diff --git a/examples/middleware/e2e/cloudflare-context.spec.ts b/examples/middleware/e2e/cloudflare-context.spec.ts new file mode 100644 index 00000000..726c52af --- /dev/null +++ b/examples/middleware/e2e/cloudflare-context.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from "@playwright/test"; + +test("cloudflare context env object is populated", async ({ page }) => { + await page.goto("/middleware"); + const cloudflareContextHeaderElement = page.getByTestId("cloudflare-context-header"); + // Note: the text in the span is "keys of `cloudflareContext.env`: MY_VAR, MY_KV, ASSETS" for previews + // and "keys of `cloudflareContext.env`: MY_VAR, MY_KV" in dev (`next dev`) + // that's why we use `toContain` instead of `toEqual`, this is incorrect and the `ASSETS` binding + // should ideally also be part of the dev cloudflare context + // (this is an upstream wrangler issue: https://github.com/cloudflare/workers-sdk/issues/7812) + expect(await cloudflareContextHeaderElement.textContent()).toContain( + "keys of `cloudflareContext.env`: MY_VAR, MY_KV" + ); +}); diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts index d6d8499c..6a1246eb 100644 --- a/examples/middleware/e2e/playwright.config.ts +++ b/examples/middleware/e2e/playwright.config.ts @@ -49,5 +49,8 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8774", reuseExistingServer: !process.env.CI, + // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, + // that's why we need a longer timeout here (we just add 10 seconds to the default 60) + timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, }, }); diff --git a/examples/middleware/e2e/playwright.dev.config.ts b/examples/middleware/e2e/playwright.dev.config.ts new file mode 100644 index 00000000..aaec40b8 --- /dev/null +++ b/examples/middleware/e2e/playwright.dev.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from "@playwright/test"; + +declare var process: { env: Record }; + +/** + * 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..33f1eb11 100644 --- a/examples/middleware/middleware.ts +++ b/examples/middleware/middleware.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse, NextFetchEvent } from "next/server"; import { clerkMiddleware } from "@clerk/nextjs/server"; -export function middleware(request: NextRequest, event: NextFetchEvent) { +import { getCloudflareContext } from "@opennextjs/cloudflare"; + +export async function middleware(request: NextRequest, event: NextFetchEvent) { console.log("middleware"); if (request.nextUrl.pathname === "/about") { return NextResponse.redirect(new URL("/redirected", request.url)); @@ -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", + `keys of \`cloudflareContext.env\`: ${Object.keys(cloudflareContext.env).join(", ")}` + ); + + 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..a42659c4 100644 --- a/examples/middleware/next.config.mjs +++ b/examples/middleware/next.config.mjs @@ -1,4 +1,8 @@ +import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; + /** @type {import('next').NextConfig} */ const nextConfig = {}; +initOpenNextCloudflareForDev(); + export default 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": "" }] } diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts new file mode 100644 index 00000000..8e435303 --- /dev/null +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -0,0 +1,175 @@ +import type { Context, RunningCodeOptions } from "node:vm"; + +declare global { + interface CloudflareEnv { + NEXT_CACHE_WORKERS_KV?: KVNamespace; + ASSETS?: Fetcher; + } +} + +export type CloudflareContext< + CfProperties extends Record = 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__"); + +/** + * Utility to get the current Cloudflare context + * + * @returns the cloudflare context + */ +export function getCloudflareContext< + CfProperties extends Record = IncomingRequestCfProperties, + Context = ExecutionContext, +>(): CloudflareContext { + const global = getInternalGlobalThis(); + + 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 used` + + ` 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 + * + * (Currently all the setup that this function performs is making sure that `getCloudflareContext` works as intended, + * in the future mode improvements might be added) + * + * Note: this function should only be called inside the Next.js config file + */ +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) { + const global = getInternalGlobalThis(); + 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) { + 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 & { + [cloudflareContextSymbol]?: CloudflareContext; + }; + 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 = IncomingRequestCfProperties, + Context = ExecutionContext, +>(): Promise> { + // 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, + }; +} + +type InternalGlobalThis< + CfProperties extends Record = IncomingRequestCfProperties, + Context = ExecutionContext, +> = typeof globalThis & { + [cloudflareContextSymbol]: CloudflareContext | undefined; +}; + +/** + * Gets the globalThis for internal usage + * + * @returns the globalThis reference casted to the InternalGlobalThis type + */ +function getInternalGlobalThis< + CfProperties extends Record = IncomingRequestCfProperties, + Context = ExecutionContext, +>(): InternalGlobalThis { + return globalThis as InternalGlobalThis; +} 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 = 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 = IncomingRequestCfProperties, - Context = ExecutionContext, ->(): Promise> { - const global = globalThis as unknown as { - [cloudflareContextSymbol]: CloudflareContext | 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 = IncomingRequestCfProperties, - Context = ExecutionContext, ->(): Promise> { - const global = globalThis as unknown as { - [cloudflareContextInNextDevSymbol]: CloudflareContext | 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..5c2652cb 100644 --- a/packages/cloudflare/src/api/kvCache.ts +++ b/packages/cloudflare/src/api/kvCache.ts @@ -2,7 +2,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"; 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: From 30a9f890d01d40fd8acd73b1f6d19340acb87f16 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 23 Jan 2025 23:41:14 +0000 Subject: [PATCH 02/20] Apply suggestions from code review Co-authored-by: Victor Berchet --- .changeset/chilly-dryers-begin.md | 4 ++-- examples/api/e2e/playwright.config.ts | 4 +--- examples/middleware/next.config.mjs | 4 ++-- packages/cloudflare/src/api/cloudflare-context.ts | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md index 3824ebc0..ecb23cfb 100644 --- a/.changeset/chilly-dryers-begin.md +++ b/.changeset/chilly-dryers-begin.md @@ -16,11 +16,11 @@ Example: import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; +initOpenNextCloudflareForDev(); + /** @type {import('next').NextConfig} */ const nextConfig = {}; -initOpenNextCloudflareForDev(); - export default nextConfig; ``` diff --git a/examples/api/e2e/playwright.config.ts b/examples/api/e2e/playwright.config.ts index 3800628b..14e21c9e 100644 --- a/examples/api/e2e/playwright.config.ts +++ b/examples/api/e2e/playwright.config.ts @@ -49,8 +49,6 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8770", reuseExistingServer: !process.env.CI, - // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, - // that's why we need a longer timeout here (we just add 10 seconds to the default 60) - timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, + timeout: 70_000, }, }); diff --git a/examples/middleware/next.config.mjs b/examples/middleware/next.config.mjs index a42659c4..24073f0a 100644 --- a/examples/middleware/next.config.mjs +++ b/examples/middleware/next.config.mjs @@ -1,8 +1,8 @@ import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"; +initOpenNextCloudflareForDev(); + /** @type {import('next').NextConfig} */ const nextConfig = {}; -initOpenNextCloudflareForDev(); - export default nextConfig; diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index 8e435303..51277393 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -54,7 +54,7 @@ export function getCloudflareContext< const getContextFunctionName = getCloudflareContext.name; const initFunctionName = initOpenNextCloudflareForDev.name; throw new Error( - `\n\n\`${getContextFunctionName}\` has been called during development without having used` + + `\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" + From a9b6b3a3b0b4f71e63d1715718e2b6cf12a03f47 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 23 Jan 2025 23:54:47 +0000 Subject: [PATCH 03/20] remove `getInternalGlobalThis` function --- .../cloudflare/src/api/cloudflare-context.ts | 34 +++++++------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index 51277393..84b52763 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -34,6 +34,17 @@ export type CloudflareContext< */ 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 = IncomingRequestCfProperties, + Context = ExecutionContext, +> = typeof globalThis & { + [cloudflareContextSymbol]: CloudflareContext | undefined; +}; + /** * Utility to get the current Cloudflare context * @@ -43,7 +54,7 @@ export function getCloudflareContext< CfProperties extends Record = IncomingRequestCfProperties, Context = ExecutionContext, >(): CloudflareContext { - const global = getInternalGlobalThis(); + const global = globalThis as InternalGlobalThis; const cloudflareContext = global[cloudflareContextSymbol]; @@ -95,7 +106,7 @@ export async function initOpenNextCloudflareForDev() { * @param cloudflareContext the cloudflare context to add to the node.sj global scope */ function addCloudflareContextToNodejsGlobal(cloudflareContext: CloudflareContext) { - const global = getInternalGlobalThis(); + const global = globalThis as InternalGlobalThis; global[cloudflareContextSymbol] = cloudflareContext; } @@ -154,22 +165,3 @@ async function getCloudflareContextFromWrangler< ctx: ctx as Context, }; } - -type InternalGlobalThis< - CfProperties extends Record = IncomingRequestCfProperties, - Context = ExecutionContext, -> = typeof globalThis & { - [cloudflareContextSymbol]: CloudflareContext | undefined; -}; - -/** - * Gets the globalThis for internal usage - * - * @returns the globalThis reference casted to the InternalGlobalThis type - */ -function getInternalGlobalThis< - CfProperties extends Record = IncomingRequestCfProperties, - Context = ExecutionContext, ->(): InternalGlobalThis { - return globalThis as InternalGlobalThis; -} From 26ae6de4e0513b4b2583395f49c482e311e6b26b Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 23 Jan 2025 23:58:56 +0000 Subject: [PATCH 04/20] mention that `initOpenNextCloudflareForDev` doesn't need to be `await`ed --- packages/cloudflare/src/api/cloudflare-context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index 84b52763..35020dae 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -89,7 +89,7 @@ export function getCloudflareContext< * (Currently all the setup that this function performs is making sure that `getCloudflareContext` works as intended, * in the future mode improvements might be added) * - * Note: this function should only be called inside the Next.js config file + * 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(); From 6c29ca779b10cda45a6f71d1f7ffa532c07bb441 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 00:04:47 +0000 Subject: [PATCH 05/20] remove `init` awaiting in kvCache --- packages/cloudflare/src/api/kvCache.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cloudflare/src/api/kvCache.ts b/packages/cloudflare/src/api/kvCache.ts index 5c2652cb..8840b1dc 100644 --- a/packages/cloudflare/src/api/kvCache.ts +++ b/packages/cloudflare/src/api/kvCache.ts @@ -26,7 +26,7 @@ class Cache implements IncrementalCache { isFetch?: IsFetch ): Promise>> { if (!this.initialized) { - await this.init(); + this.init(); } if (!(this.kv || this.assets)) { @@ -79,7 +79,7 @@ class Cache implements IncrementalCache { isFetch?: IsFetch ): Promise { if (!this.initialized) { - await this.init(); + this.init(); } if (!this.kv) { throw new IgnorableError(`No KVNamespace`); @@ -106,7 +106,7 @@ class Cache implements IncrementalCache { async delete(key: string): Promise { if (!this.initialized) { - await this.init(); + this.init(); } if (!this.kv) { throw new IgnorableError(`No KVNamespace`); @@ -141,8 +141,8 @@ class Cache implements IncrementalCache { return process.env.NEXT_BUILD_ID ?? "no-build-id"; } - protected async init() { - const env = (await getCloudflareContext()).env; + protected init() { + const { env } = getCloudflareContext(); this.kv = env.NEXT_CACHE_WORKERS_KV; this.assets = env.ASSETS; this.initialized = true; From 6c54ae334efcfac3033886432cbbc039d5807be2 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 00:45:45 +0000 Subject: [PATCH 06/20] update test so that ordering of env keys does not matter --- examples/middleware/e2e/cloudflare-context.spec.ts | 7 +------ examples/middleware/middleware.ts | 5 ++++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/examples/middleware/e2e/cloudflare-context.spec.ts b/examples/middleware/e2e/cloudflare-context.spec.ts index 726c52af..51f4d7ca 100644 --- a/examples/middleware/e2e/cloudflare-context.spec.ts +++ b/examples/middleware/e2e/cloudflare-context.spec.ts @@ -3,12 +3,7 @@ import { test, expect } from "@playwright/test"; test("cloudflare context env object is populated", async ({ page }) => { await page.goto("/middleware"); const cloudflareContextHeaderElement = page.getByTestId("cloudflare-context-header"); - // Note: the text in the span is "keys of `cloudflareContext.env`: MY_VAR, MY_KV, ASSETS" for previews - // and "keys of `cloudflareContext.env`: MY_VAR, MY_KV" in dev (`next dev`) - // that's why we use `toContain` instead of `toEqual`, this is incorrect and the `ASSETS` binding - // should ideally also be part of the dev cloudflare context - // (this is an upstream wrangler issue: https://github.com/cloudflare/workers-sdk/issues/7812) expect(await cloudflareContextHeaderElement.textContent()).toContain( - "keys of `cloudflareContext.env`: MY_VAR, MY_KV" + "variables from `cloudflareContext.env`: MY_KV, MY_VAR" ); }); diff --git a/examples/middleware/middleware.ts b/examples/middleware/middleware.ts index 33f1eb11..3131f507 100644 --- a/examples/middleware/middleware.ts +++ b/examples/middleware/middleware.ts @@ -23,7 +23,10 @@ export async function middleware(request: NextRequest, event: NextFetchEvent) { requestHeaders.set( "x-cloudflare-context", - `keys of \`cloudflareContext.env\`: ${Object.keys(cloudflareContext.env).join(", ")}` + `variables from \`cloudflareContext.env\`: ${Object.keys(cloudflareContext.env) + .filter((key) => key.startsWith("MY_")) + .sort() + .join(", ")}` ); return NextResponse.next({ From c91b36951e8b9bb44525619c7748dbc1fcb75ae1 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 24 Jan 2025 07:26:00 +0100 Subject: [PATCH 07/20] fixup! update kvCache --- package.json | 1 + packages/cloudflare/src/api/kvCache.ts | 49 +++++++++++--------------- 2 files changed, 21 insertions(+), 29 deletions(-) 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/src/api/kvCache.ts b/packages/cloudflare/src/api/kvCache.ts index 8840b1dc..e7203a05 100644 --- a/packages/cloudflare/src/api/kvCache.ts +++ b/packages/cloudflare/src/api/kvCache.ts @@ -1,4 +1,3 @@ -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"; @@ -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( key: string, isFetch?: IsFetch ): Promise>> { - if (!this.initialized) { - 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 ): Promise { - if (!this.initialized) { - 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 { - if (!this.initialized) { - 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 init() { - const { env } = getCloudflareContext(); - this.kv = env.NEXT_CACHE_WORKERS_KV; - this.assets = env.ASSETS; - this.initialized = true; - } } export default new Cache(); From 6e9e5001eea719177db93d0622d37848882bbc8f Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Fri, 24 Jan 2025 07:40:48 +0100 Subject: [PATCH 08/20] fixup! drop async --- examples/api/app/api/hello/route.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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) { From a3b455130c649256657f00f9afd9e92e4bf31d7b Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 09:59:31 +0000 Subject: [PATCH 09/20] remove `getCloudflareContext` example from changeset --- .changeset/chilly-dryers-begin.md | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md index ecb23cfb..ce2751d5 100644 --- a/.changeset/chilly-dryers-begin.md +++ b/.changeset/chilly-dryers-begin.md @@ -25,23 +25,3 @@ export default nextConfig; ``` thanks to this new utility we are also able to make `getCloudflareContext` synchronous, so `await`ing such function is no longer necessary - -Example: - -- Before: - - ```js - import { getCloudflareContext } from "@opennextjs/cloudflare"; - - const cloudflareContext = await getCloudflareContext(); - // cloudflareContext use - ``` - -- After: - - ```js - import { getCloudflareContext } from "@opennextjs/cloudflare"; - - const cloudflareContext = getCloudflareContext(); - // cloudflareContext use - ``` From 1d76b5fd73c2e26a0b68fd7803f100892f2e50af Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:00:44 +0000 Subject: [PATCH 10/20] apply requested timout change --- examples/create-next-app/e2e/playwright.config.ts | 4 +--- examples/middleware/e2e/playwright.config.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/create-next-app/e2e/playwright.config.ts b/examples/create-next-app/e2e/playwright.config.ts index e44a9e0a..a822d9ce 100644 --- a/examples/create-next-app/e2e/playwright.config.ts +++ b/examples/create-next-app/e2e/playwright.config.ts @@ -49,8 +49,6 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8771", reuseExistingServer: !process.env.CI, - // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, - // that's why we need a longer timeout here (we just add 10 seconds to the default 60) - timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, + timeout: 7_000, }, }); diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts index 6a1246eb..3814449a 100644 --- a/examples/middleware/e2e/playwright.config.ts +++ b/examples/middleware/e2e/playwright.config.ts @@ -49,8 +49,6 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8774", reuseExistingServer: !process.env.CI, - // the app uses the `initOpenNextCloudflareForDev` which in CI apparently makes the boot up take slightly longer, - // that's why we need a longer timeout here (we just add 10 seconds to the default 60) - timeout: !process.env.CI ? undefined : 60 * 1000 + 10 * 1000, + timeout: 7_000, }, }); From e942a91b3a2608d9075e7aa0cd3a0acf7b4ebb6f Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:04:15 +0000 Subject: [PATCH 11/20] amend wrong timeout --- examples/create-next-app/e2e/playwright.config.ts | 2 +- examples/middleware/e2e/playwright.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/create-next-app/e2e/playwright.config.ts b/examples/create-next-app/e2e/playwright.config.ts index a822d9ce..aa1e233f 100644 --- a/examples/create-next-app/e2e/playwright.config.ts +++ b/examples/create-next-app/e2e/playwright.config.ts @@ -49,6 +49,6 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8771", reuseExistingServer: !process.env.CI, - timeout: 7_000, + timeout: 70_000, }, }); diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts index 3814449a..cf08878f 100644 --- a/examples/middleware/e2e/playwright.config.ts +++ b/examples/middleware/e2e/playwright.config.ts @@ -49,6 +49,6 @@ export default defineConfig({ command: "pnpm preview:worker", url: "http://localhost:8774", reuseExistingServer: !process.env.CI, - timeout: 7_000, + timeout: 70_000, }, }); From 8b8a25d2b766ae73115c57e223883e61d095fc09 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:16:45 +0000 Subject: [PATCH 12/20] update process declaration in playwright configs --- examples/api/e2e/playwright.config.ts | 3 ++- examples/api/e2e/playwright.dev.config.ts | 3 ++- examples/create-next-app/e2e/playwright.config.ts | 3 ++- examples/middleware/e2e/playwright.config.ts | 3 ++- examples/middleware/e2e/playwright.dev.config.ts | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/api/e2e/playwright.config.ts b/examples/api/e2e/playwright.config.ts index 14e21c9e..02e4d9d1 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 nodeProcess from "node:process"; -declare var process: { env: Record }; +declare const process: typeof nodeProcess; /** * See https://playwright.dev/docs/test-configuration. diff --git a/examples/api/e2e/playwright.dev.config.ts b/examples/api/e2e/playwright.dev.config.ts index d3d61c03..89fbbf10 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 nodeProcess from "node:process"; -declare var process: { env: Record }; +declare const process: typeof nodeProcess; /** * See https://playwright.dev/docs/test-configuration. diff --git a/examples/create-next-app/e2e/playwright.config.ts b/examples/create-next-app/e2e/playwright.config.ts index aa1e233f..99968ccc 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 nodeProcess from "node:process"; -declare const process: { env: Record }; +declare const process: typeof nodeProcess; /** * See https://playwright.dev/docs/test-configuration. diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts index cf08878f..b3240ecb 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 nodeProcess from "node:process"; -declare const process: { env: Record }; +declare const process: typeof nodeProcess; /** * See https://playwright.dev/docs/test-configuration. diff --git a/examples/middleware/e2e/playwright.dev.config.ts b/examples/middleware/e2e/playwright.dev.config.ts index aaec40b8..cb868244 100644 --- a/examples/middleware/e2e/playwright.dev.config.ts +++ b/examples/middleware/e2e/playwright.dev.config.ts @@ -1,6 +1,7 @@ import { defineConfig, devices } from "@playwright/test"; +import nodeProcess from "node:process"; -declare var process: { env: Record }; +declare const process: typeof nodeProcess; /** * See https://playwright.dev/docs/test-configuration. From 7bf0790524c673c1511c677af1047426f896dd14 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:31:18 +0000 Subject: [PATCH 13/20] simplify middleware e2e check --- examples/middleware/e2e/cloudflare-context.spec.ts | 4 ++-- examples/middleware/middleware.ts | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/middleware/e2e/cloudflare-context.spec.ts b/examples/middleware/e2e/cloudflare-context.spec.ts index 51f4d7ca..ed27b846 100644 --- a/examples/middleware/e2e/cloudflare-context.spec.ts +++ b/examples/middleware/e2e/cloudflare-context.spec.ts @@ -1,9 +1,9 @@ import { test, expect } from "@playwright/test"; -test("cloudflare context env object is populated", async ({ page }) => { +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( - "variables from `cloudflareContext.env`: MY_KV, MY_VAR" + "typeof `cloudflareContext.env` = object" ); }); diff --git a/examples/middleware/middleware.ts b/examples/middleware/middleware.ts index 3131f507..c6d88b15 100644 --- a/examples/middleware/middleware.ts +++ b/examples/middleware/middleware.ts @@ -23,10 +23,7 @@ export async function middleware(request: NextRequest, event: NextFetchEvent) { requestHeaders.set( "x-cloudflare-context", - `variables from \`cloudflareContext.env\`: ${Object.keys(cloudflareContext.env) - .filter((key) => key.startsWith("MY_")) - .sort() - .join(", ")}` + `typeof \`cloudflareContext.env\` = ${typeof cloudflareContext.env}` ); return NextResponse.next({ From 3b1deb3597ae56130cb27791cbf18525558df4be Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:36:32 +0000 Subject: [PATCH 14/20] import type from "node:process" --- examples/api/e2e/playwright.config.ts | 2 +- examples/api/e2e/playwright.dev.config.ts | 2 +- examples/create-next-app/e2e/playwright.config.ts | 2 +- examples/middleware/e2e/playwright.config.ts | 2 +- examples/middleware/e2e/playwright.dev.config.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/api/e2e/playwright.config.ts b/examples/api/e2e/playwright.config.ts index 02e4d9d1..0f5f526d 100644 --- a/examples/api/e2e/playwright.config.ts +++ b/examples/api/e2e/playwright.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; -import nodeProcess from "node:process"; +import type nodeProcess from "node:process"; declare const process: typeof nodeProcess; diff --git a/examples/api/e2e/playwright.dev.config.ts b/examples/api/e2e/playwright.dev.config.ts index 89fbbf10..b14e729e 100644 --- a/examples/api/e2e/playwright.dev.config.ts +++ b/examples/api/e2e/playwright.dev.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; -import nodeProcess from "node:process"; +import type nodeProcess from "node:process"; declare const process: typeof nodeProcess; diff --git a/examples/create-next-app/e2e/playwright.config.ts b/examples/create-next-app/e2e/playwright.config.ts index 99968ccc..083a501c 100644 --- a/examples/create-next-app/e2e/playwright.config.ts +++ b/examples/create-next-app/e2e/playwright.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; -import nodeProcess from "node:process"; +import type nodeProcess from "node:process"; declare const process: typeof nodeProcess; diff --git a/examples/middleware/e2e/playwright.config.ts b/examples/middleware/e2e/playwright.config.ts index b3240ecb..66c22aa5 100644 --- a/examples/middleware/e2e/playwright.config.ts +++ b/examples/middleware/e2e/playwright.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; -import nodeProcess from "node:process"; +import type nodeProcess from "node:process"; declare const process: typeof nodeProcess; diff --git a/examples/middleware/e2e/playwright.dev.config.ts b/examples/middleware/e2e/playwright.dev.config.ts index cb868244..6fabd3da 100644 --- a/examples/middleware/e2e/playwright.dev.config.ts +++ b/examples/middleware/e2e/playwright.dev.config.ts @@ -1,5 +1,5 @@ import { defineConfig, devices } from "@playwright/test"; -import nodeProcess from "node:process"; +import type nodeProcess from "node:process"; declare const process: typeof nodeProcess; From 99905195e55349c29c27dfb60af1c907e691dafa Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 10:43:28 +0000 Subject: [PATCH 15/20] update changeset --- .changeset/chilly-dryers-begin.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md index ce2751d5..ccbb50a4 100644 --- a/.changeset/chilly-dryers-begin.md +++ b/.changeset/chilly-dryers-begin.md @@ -4,10 +4,13 @@ introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareContext` synchronous -introduce a new `initOpenNextCloudflareForDev` function that when called in the [Next.js config file](https://nextjs.org/docs/app/api-reference/config/next-config-js) integrates the Next.js dev server with the open-next Cloudflare adapter. Most noticeably this enables `getCloudflareContext` to work in -the edge runtime (including middlewares) during development. +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. -Moving forward we'll recommend that all applications include the use of this utility in their config file (there is no downside in doing so and it only effect local development). +Also makes `getCloudflareContext` synchronous, so `await`ing such function is no longer necessary. + +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: @@ -23,5 +26,3 @@ const nextConfig = {}; export default nextConfig; ``` - -thanks to this new utility we are also able to make `getCloudflareContext` synchronous, so `await`ing such function is no longer necessary From a973ae532f8e786dd478194a3db81a7d8c14742d Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 22:36:12 +0000 Subject: [PATCH 16/20] Apply suggestions from code review Co-authored-by: Victor Berchet --- .changeset/chilly-dryers-begin.md | 2 +- packages/cloudflare/src/api/cloudflare-context.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md index ccbb50a4..ff6a8cff 100644 --- a/.changeset/chilly-dryers-begin.md +++ b/.changeset/chilly-dryers-begin.md @@ -6,7 +6,7 @@ introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareCont 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, so `await`ing such function is no longer necessary. +Also makes `getCloudflareContext` synchronous. Additionally the `getCloudflareContext` can now work during local development (`next dev`) in the edge runtime (including middlewares). diff --git a/packages/cloudflare/src/api/cloudflare-context.ts b/packages/cloudflare/src/api/cloudflare-context.ts index 35020dae..e9c39edf 100644 --- a/packages/cloudflare/src/api/cloudflare-context.ts +++ b/packages/cloudflare/src/api/cloudflare-context.ts @@ -86,9 +86,6 @@ export function getCloudflareContext< * 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 * - * (Currently all the setup that this function performs is making sure that `getCloudflareContext` works as intended, - * in the future mode improvements might be added) - * * 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() { From 612f3f703a80ea77979424bc483d5f71a70dd75f Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 22:56:30 +0000 Subject: [PATCH 17/20] update changeset to minor --- .changeset/chilly-dryers-begin.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chilly-dryers-begin.md b/.changeset/chilly-dryers-begin.md index ff6a8cff..40ce3f52 100644 --- a/.changeset/chilly-dryers-begin.md +++ b/.changeset/chilly-dryers-begin.md @@ -1,5 +1,5 @@ --- -"@opennextjs/cloudflare": patch +"@opennextjs/cloudflare": minor --- introduce new `initOpenNextCloudflareForDev` utility and make `getCloudflareContext` synchronous From 6473145cca5470df8a4b78b9909eba7bced0a77a Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Fri, 24 Jan 2025 23:01:20 +0000 Subject: [PATCH 18/20] remove getting started info from README and redirect to official docs --- packages/cloudflare/README.md | 62 +---------------------------------- 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index fe083089..45435298 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -6,67 +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": "", - "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; -``` +To get started with the adapter visit the [official get started documentation](https://opennext.js.org/cloudflare/get-started). ## Known issues From 7a272cbf65f8cbaa6f58b46f875be5f5a20152cf Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 27 Jan 2025 09:56:52 +0000 Subject: [PATCH 19/20] Update examples/middleware/middleware.ts Co-authored-by: Victor Berchet --- examples/middleware/middleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/middleware/middleware.ts b/examples/middleware/middleware.ts index c6d88b15..d7facf06 100644 --- a/examples/middleware/middleware.ts +++ b/examples/middleware/middleware.ts @@ -3,7 +3,7 @@ import { clerkMiddleware } from "@clerk/nextjs/server"; import { getCloudflareContext } from "@opennextjs/cloudflare"; -export async function middleware(request: NextRequest, event: NextFetchEvent) { +export function middleware(request: NextRequest, event: NextFetchEvent) { console.log("middleware"); if (request.nextUrl.pathname === "/about") { return NextResponse.redirect(new URL("/redirected", request.url)); From ed349ca035e1a664d2f9d7f1ab7617191cd9f79c Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Mon, 27 Jan 2025 10:00:52 +0000 Subject: [PATCH 20/20] remove known issues section from readme --- packages/cloudflare/README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 45435298..bfc0336a 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -8,11 +8,6 @@ Deploy Next.js apps to Cloudflare! To get started with the adapter visit the [official get started documentation](https://opennext.js.org/cloudflare/get-started). -## Known issues - -- `▲ [WARNING] Suspicious assignment to defined constant "process.env.NODE_ENV" [assign-to-define]` can safely be ignored -- Maybe more, still experimental... - ## Local development - you can use the regular `next` CLI to start the Next.js dev server: