-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
enable
getCloudflareContext
to work in middlewares via a new `enabl…
…eEdgeDevGetCloudflareContext` utility
- Loading branch information
1 parent
0b54cf4
commit 59419ca
Showing
13 changed files
with
301 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
--- | ||
"@opennextjs/cloudflare": patch | ||
--- | ||
|
||
enable `getCloudflareContext` to work in middlewares via a new `enableEdgeDevGetCloudflareContext` utility | ||
|
||
`getCloudflareContext` can't work, during development (with `next dev`), in middlewares since they run in | ||
the edge runtime (see: https://nextjs.org/docs/app/building-your-application/routing/middleware#runtime) which | ||
is incompatible with the `wrangler`'s APIs which run in node.js | ||
|
||
To solve this a new utility called `enableEdgeDevGetCloudflareContext` has been introduced that allows the | ||
context to be available also in the edge runtime, the utility needs to be called inside the Next.js config | ||
file, for example: | ||
|
||
```js | ||
// next.config.mjs | ||
|
||
import { enableEdgeDevGetCloudflareContext } from "@opennextjs/cloudflare"; | ||
|
||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = {}; | ||
|
||
enableEdgeDevGetCloudflareContext(); | ||
|
||
export default nextConfig; | ||
``` | ||
|
||
a helpful error is also thrown in `getCloudflareContext` to prompt developers to use the utility | ||
|
||
`getCloudflareContext` called in the nodejs runtime works as before without needing any setup |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { defineConfig, devices } from "@playwright/test"; | ||
|
||
declare var process: { env: Record<string, string> }; | ||
|
||
/** | ||
* 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, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,8 @@ | ||
import { enableEdgeDevGetCloudflareContext } from "@opennextjs/cloudflare"; | ||
|
||
/** @type {import('next').NextConfig} */ | ||
const nextConfig = {}; | ||
|
||
enableEdgeDevGetCloudflareContext(); | ||
|
||
export default nextConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
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]) { | ||
try { | ||
const cloudflareContext = await getCloudflareContextFromWrangler<CfProperties, Context>(); | ||
global[cloudflareContextInNextDevSymbol] = cloudflareContext; | ||
} catch (e: unknown) { | ||
if (e instanceof Error && e.message.includes("A dynamic import callback was not specified")) { | ||
const getCloudflareContextFunctionName = getCloudflareContext.name; | ||
const enablingFunctionName = enableEdgeDevGetCloudflareContext.name; | ||
throw new Error( | ||
`\n\n\`${getCloudflareContextFunctionName}\` has been invoked during development inside the edge runtime ` + | ||
"this is not enabled, to enable such use of the function you need to import and call the " + | ||
`\`${enablingFunctionName}\` function inside your Next.js config file\n\n"` + | ||
"Example: \n ```\n // next.config.mjs\n\n" + | ||
` import { ${enablingFunctionName} } from "@opennextjs/cloudflare";\n\n` + | ||
` ${enablingFunctionName}();\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" | ||
); | ||
} else { | ||
throw e; | ||
} | ||
} | ||
} | ||
|
||
return global[cloudflareContextInNextDevSymbol]!; | ||
} | ||
|
||
type RuntimeContext = Record<string, unknown> & { | ||
process?: { env?: Record<string | symbol, unknown> }; | ||
[cloudflareContextInNextDevSymbol]?: { | ||
env: unknown; | ||
ctx: unknown; | ||
cf: unknown; | ||
}; | ||
}; | ||
|
||
/** | ||
* Enables `getCloudflareContext` to work, during development (via `next dev`), in the edge runtime | ||
* | ||
* Note: currently middlewares always run in the edge runtime | ||
* | ||
* Note: this function should only be called inside the Next.js config file | ||
*/ | ||
export async function enableEdgeDevGetCloudflareContext() { | ||
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); | ||
|
||
const context = await getCloudflareContextFromWrangler(); | ||
|
||
vmModule.runInContext = (...args: [string, RuntimeContext, ...unknown[]]) => { | ||
const runtimeContext = args[1]; | ||
runtimeContext[cloudflareContextInNextDevSymbol] ??= context; | ||
return originalRunInContext(...args); | ||
}; | ||
} | ||
|
||
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, | ||
}; | ||
} |
Oops, something went wrong.