-
Notifications
You must be signed in to change notification settings - Fork 1k
add remote bindings support to getPlatformProxy
#9688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
42c2d23
10e7f66
d3bcc90
d9f0a7f
b44fad4
0e1b23c
41ed675
2a248ae
b67b22f
3684fa7
90aed0a
e78b859
5f1d352
869a6fd
86f2189
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| --- | ||
| "wrangler": patch | ||
| --- | ||
|
|
||
| add remote bindings support to `getPlatformProxy` | ||
|
|
||
| Example: | ||
|
|
||
| ```json | ||
| // wrangler.jsonc | ||
| { | ||
| "name": "get-platform-proxy-test", | ||
| "services": [ | ||
| { | ||
| "binding": "MY_WORKER", | ||
| "service": "my-worker", | ||
| "experimental_remote": true | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ```js | ||
| // index.mjs | ||
| import { getPlatformProxy } from "wrangler"; | ||
|
|
||
| const { env } = await getPlatformProxy({ | ||
| experimental: { | ||
| remoteBindings: true, | ||
| }, | ||
| }); | ||
|
|
||
| // env.MY_WORKER.fetch() fetches from the remote my-worker service | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { execSync } from "child_process"; | ||
| import { randomUUID } from "crypto"; | ||
| import assert from "node:assert"; | ||
| import { mkdirSync, rmSync, writeFileSync } from "node:fs"; | ||
| import test, { after, before, describe } from "node:test"; | ||
| import { getPlatformProxy } from "wrangler"; | ||
|
|
||
| if ( | ||
| !process.env.TEST_CLOUDFLARE_API_TOKEN || | ||
| !process.env.TEST_CLOUDFLARE_ACCOUNT_ID | ||
| ) { | ||
| console.warn("No credentials provided, skipping test..."); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| const env = { | ||
| ...process.env, | ||
| CLOUDFLARE_API_TOKEN: process.env.TEST_CLOUDFLARE_API_TOKEN, | ||
| CLOUDFLARE_ACCOUNT_ID: process.env.TEST_CLOUDFLARE_ACCOUNT_ID, | ||
| }; | ||
|
|
||
| describe("getPlatformProxy remote-bindings", () => { | ||
| const remoteWorkerName = `get-platform-proxy-remote-worker-test-${randomUUID().split("-")[0]}`; | ||
|
|
||
| before(async () => { | ||
| // Note: ideally we pass the auth data to `getPlatformProxy`, that currently is not | ||
dario-piotrowicz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // possible (DEVX-1857) so we need to make sure that the CLOUDFLARE_ACCOUNT_ID | ||
| // and CLOUDFLARE_API_TOKEN env variables are set so that `getPlatformProxy` | ||
| // can establish the remote proxy connection | ||
| process.env.CLOUDFLARE_ACCOUNT_ID = process.env.TEST_CLOUDFLARE_ACCOUNT_ID; | ||
| process.env.CLOUDFLARE_API_TOKEN = process.env.TEST_CLOUDFLARE_API_TOKEN; | ||
|
|
||
| const deployOut = execSync( | ||
| `pnpm dlx wrangler deploy remote-worker.js --name ${remoteWorkerName} --compatibility-date 2025-06-19`, | ||
| { | ||
| stdio: "pipe", | ||
| env, | ||
| } | ||
| ); | ||
|
|
||
| if ( | ||
| !new RegExp(`Deployed\\s+${remoteWorkerName}\\b`).test(`${deployOut}`) | ||
| ) { | ||
| throw new Error(`Failed to deploy ${remoteWorkerName}`); | ||
| } | ||
|
|
||
| rmSync("./.tmp", { recursive: true, force: true }); | ||
|
|
||
| mkdirSync("./.tmp"); | ||
|
|
||
| writeFileSync( | ||
| "./.tmp/wrangler.json", | ||
| JSON.stringify( | ||
| { | ||
| name: "get-platform-proxy-fixture-test", | ||
| compatibility_date: "2025-06-01", | ||
| services: [ | ||
| { | ||
| binding: "MY_WORKER", | ||
| service: remoteWorkerName, | ||
| experimental_remote: true, | ||
| }, | ||
| ], | ||
| }, | ||
| undefined, | ||
| 2 | ||
| ), | ||
| "utf8" | ||
| ); | ||
| }); | ||
|
|
||
| test("getPlatformProxy works with remote bindings", async () => { | ||
| const { env, dispose } = await getPlatformProxy({ | ||
| configPath: "./.tmp/wrangler.json", | ||
| experimental: { remoteBindings: true }, | ||
| }); | ||
|
|
||
| try { | ||
| assert.strictEqual( | ||
| await (await env.MY_WORKER.fetch("http://example.com")).text(), | ||
| "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" | ||
| ); | ||
| } finally { | ||
| await dispose(); | ||
| } | ||
| }); | ||
|
|
||
| after(async () => { | ||
| execSync(`pnpm dlx wrangler delete --name ${remoteWorkerName}`, { env }); | ||
| rmSync("./.tmp", { recursive: true, force: true }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "@fixture/get-platform-proxy-remote-bindings-node-test", | ||
| "private": true, | ||
| "description": "", | ||
| "license": "ISC", | ||
| "author": "", | ||
| "type": "module", | ||
| "scripts": { | ||
| "test:ci": "node --test" | ||
| }, | ||
| "devDependencies": { | ||
| "@cloudflare/workers-tsconfig": "workspace:*", | ||
| "@cloudflare/workers-types": "^4.20250617.0", | ||
| "wrangler": "workspace:*" | ||
| }, | ||
| "volta": { | ||
| "extends": "../../package.json" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| export default { | ||
| fetch() { | ||
| return new Response( | ||
| "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" | ||
| ); | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,15 +11,16 @@ import { | |
| buildMiniflareBindingOptions, | ||
| buildSitesOptions, | ||
| } from "../../../dev/miniflare"; | ||
| import { run } from "../../../experimental-flags"; | ||
| import { logger } from "../../../logger"; | ||
| import { getSiteAssetPaths } from "../../../sites"; | ||
| import { dedent } from "../../../utils/dedent"; | ||
| import { maybeStartOrUpdateRemoteProxySession } from "../../remoteBindings"; | ||
| import { CacheStorage } from "./caches"; | ||
| import { ExecutionContext } from "./executionContext"; | ||
| import { getServiceBindings } from "./services"; | ||
| import type { AssetsOptions } from "../../../assets"; | ||
| import type { Config, RawConfig, RawEnvironment } from "../../../config"; | ||
| import type { RemoteProxySession } from "../../remoteBindings"; | ||
| import type { IncomingRequestCfProperties } from "@cloudflare/workers-types/experimental"; | ||
| import type { | ||
| MiniflareOptions, | ||
|
|
@@ -58,6 +59,13 @@ export type GetPlatformProxyOptions = { | |
| * If `false` is specified no data is persisted on the filesystem. | ||
| */ | ||
| persist?: boolean | { path: string }; | ||
| /** | ||
| * Experimental flags (note: these can change at any time and are not version-controlled use at your own risk) | ||
| */ | ||
| experimental?: { | ||
| /** whether access to remove bindings should be enabled */ | ||
| remoteBindings?: boolean; | ||
| }; | ||
| }; | ||
|
|
||
| /** | ||
|
|
@@ -103,29 +111,33 @@ export async function getPlatformProxy< | |
| >( | ||
| options: GetPlatformProxyOptions = {} | ||
| ): Promise<PlatformProxy<Env, CfProperties>> { | ||
| const env = options.environment; | ||
| const experimentalRemoteBindings = !!options.experimental?.remoteBindings; | ||
|
|
||
| const targetEnvironment = options.environment; | ||
dario-piotrowicz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const rawConfig = readConfig({ | ||
| config: options.configPath, | ||
| env, | ||
| env: targetEnvironment, | ||
| }); | ||
|
|
||
| const miniflareOptions = await run( | ||
| { | ||
| MULTIWORKER: false, | ||
| RESOURCES_PROVISION: false, | ||
| // TODO: when possible remote bindings should be made available for getPlatformProxy | ||
| REMOTE_BINDINGS: false, | ||
| }, | ||
| () => getMiniflareOptionsFromConfig(rawConfig, env, options) | ||
| ); | ||
| let remoteProxySession: RemoteProxySession | undefined = undefined; | ||
| if (experimentalRemoteBindings && rawConfig.configPath) { | ||
| const maybeRemoteProxySessionWrap = | ||
dario-piotrowicz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await maybeStartOrUpdateRemoteProxySession(rawConfig.configPath); | ||
| remoteProxySession = maybeRemoteProxySessionWrap?.session; | ||
| } | ||
|
|
||
| const mf = new Miniflare({ | ||
| script: "", | ||
| modules: true, | ||
| ...(miniflareOptions as Record<string, unknown>), | ||
| const miniflareOptions = await getMiniflareOptionsFromConfig({ | ||
| rawConfig, | ||
| targetEnvironment, | ||
| options, | ||
| remoteProxyConnectionString: | ||
| remoteProxySession?.remoteProxyConnectionString, | ||
| remoteBindingsEnabled: experimentalRemoteBindings, | ||
| }); | ||
|
|
||
| const mf = new Miniflare(miniflareOptions); | ||
|
|
||
| const bindings: Env = await mf.getBindings(); | ||
|
|
||
| const cf = await mf.getCf(); | ||
|
|
@@ -136,17 +148,40 @@ export async function getPlatformProxy< | |
| cf: cf as CfProperties, | ||
| ctx: new ExecutionContext(), | ||
| caches: new CacheStorage(), | ||
| dispose: () => mf.dispose(), | ||
| dispose: async () => { | ||
| await remoteProxySession?.dispose(); | ||
| await mf.dispose(); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| // this is only used by getPlatformProxy | ||
| async function getMiniflareOptionsFromConfig( | ||
| rawConfig: Config, | ||
| env: string | undefined, | ||
| options: GetPlatformProxyOptions | ||
| ): Promise<Partial<MiniflareOptions>> { | ||
| const bindings = getBindings(rawConfig, env, true, {}); | ||
| /** | ||
| * Builds an options configuration object for the `getPlatformProxy` functionality that | ||
| * can be then passed to the Miniflare constructor | ||
| * | ||
| * @param args.rawConfig The raw configuration to base the options from | ||
dario-piotrowicz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| * @param args.targetEnvironment The target environment from which to get the binding configuration options | ||
| * @param args.options The user provided `getPlatformProxy` options | ||
| * @param args.remoteProxyConnectionString The potential remote proxy connection string to be used to connect the remote bindings | ||
| * @param args.remoteBindingsEnabled Whether remote bindings are enabled | ||
| * @returns an object ready to be passed to the Miniflare constructor | ||
| */ | ||
| async function getMiniflareOptionsFromConfig(args: { | ||
| rawConfig: Config; | ||
| targetEnvironment: string | undefined; | ||
| options: GetPlatformProxyOptions; | ||
| remoteProxyConnectionString?: RemoteProxyConnectionString; | ||
| remoteBindingsEnabled: boolean; | ||
| }): Promise<MiniflareOptions> { | ||
| const { | ||
| rawConfig, | ||
| targetEnvironment, | ||
| options, | ||
| remoteProxyConnectionString, | ||
| remoteBindingsEnabled, | ||
| } = args; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, but this destructing seems a bit messy to me.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I totally agree I was simply passing the values as an object as requested, I am happy to either revert the change or proceed any way you see fit (like always passing
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer the current version FWIW
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's pretty much entirely a style choice, so I'll leave it up to your judgement @dario-piotrowicz, but fwiw I don't think it necessarily makes sense to change this function to accept an object of options. |
||
|
|
||
| const bindings = getBindings(rawConfig, targetEnvironment, true, {}); | ||
|
|
||
| if (rawConfig["durable_objects"]) { | ||
| const { localBindings } = partitionDurableObjectBindings(rawConfig); | ||
|
|
@@ -183,8 +218,8 @@ async function getMiniflareOptionsFromConfig( | |
| containers: undefined, | ||
| containerBuildId: undefined, | ||
| }, | ||
| undefined, | ||
| false | ||
| remoteProxyConnectionString, | ||
| remoteBindingsEnabled | ||
| ); | ||
|
|
||
| const defaultPersistRoot = getMiniflarePersistRoot(options.persist); | ||
|
|
@@ -208,7 +243,11 @@ async function getMiniflareOptionsFromConfig( | |
| defaultPersistRoot, | ||
| }; | ||
|
|
||
| return miniflareOptions; | ||
| return { | ||
| script: "", | ||
| modules: true, | ||
| ...miniflareOptions, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.