Skip to content

Commit 086e29d

Browse files
add remote bindings support to getPlatformProxy (cloudflare#9688)
* add remote bindings support to `getPlatformProxy` * fixup! add remote bindings support to `getPlatformProxy` put back explicit `remoteBindingsEnabled` * fixup! add remote bindings support to `getPlatformProxy` replace @ts-ignore to the more appropriate @ts-expect-error * fixup! add remote bindings support to `getPlatformProxy` replace variable `a` * fixup! add remote bindings support to `getPlatformProxy` make `preExistingRemoteProxySessionData` param optional * fixup! add remote bindings support to `getPlatformProxy` set the process.env cloudflare id and token so that getPlatformProxy can inherit them * fixup! add remote bindings support to `getPlatformProxy` add helpful comment * fixup! add remote bindings support to `getPlatformProxy` refactor `getMiniflareOptionsFromConfig` * fixup! add remote bindings support to `getPlatformProxy` pass an args object to `getMiniflareOptionsFromConfig` * fixup! add remote bindings support to `getPlatformProxy` add example to changelog * fixup! add remote bindings support to `getPlatformProxy` remove unnecessary `env` object * fixup! add remote bindings support to `getPlatformProxy` remove unnecessary intermediary variable * revert targetEnvironment variable renaming * remove extra env passing * rename rawConfig to just config
1 parent 5162c51 commit 086e29d

File tree

7 files changed

+230
-37
lines changed

7 files changed

+230
-37
lines changed

.changeset/better-sheep-reply.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
add remote bindings support to `getPlatformProxy`
6+
7+
Example:
8+
9+
```json
10+
// wrangler.jsonc
11+
{
12+
"name": "get-platform-proxy-test",
13+
"services": [
14+
{
15+
"binding": "MY_WORKER",
16+
"service": "my-worker",
17+
"experimental_remote": true
18+
}
19+
]
20+
}
21+
```
22+
23+
```js
24+
// index.mjs
25+
import { getPlatformProxy } from "wrangler";
26+
27+
const { env } = await getPlatformProxy({
28+
experimental: {
29+
remoteBindings: true,
30+
},
31+
});
32+
33+
// env.MY_WORKER.fetch() fetches from the remote my-worker service
34+
```
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { execSync } from "child_process";
2+
import { randomUUID } from "crypto";
3+
import assert from "node:assert";
4+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5+
import test, { after, before, describe } from "node:test";
6+
import { getPlatformProxy } from "wrangler";
7+
8+
if (
9+
!process.env.TEST_CLOUDFLARE_API_TOKEN ||
10+
!process.env.TEST_CLOUDFLARE_ACCOUNT_ID
11+
) {
12+
console.warn("No credentials provided, skipping test...");
13+
process.exit(0);
14+
}
15+
16+
describe("getPlatformProxy remote-bindings", () => {
17+
const remoteWorkerName = `get-platform-proxy-remote-worker-test-${randomUUID().split("-")[0]}`;
18+
19+
before(async () => {
20+
// Note: ideally we pass the auth data to `getPlatformProxy`, that currently is not
21+
// possible (DEVX-1857) so we need to make sure that the CLOUDFLARE_ACCOUNT_ID
22+
// and CLOUDFLARE_API_TOKEN env variables are set so that `getPlatformProxy`
23+
// can establish the remote proxy connection
24+
process.env.CLOUDFLARE_ACCOUNT_ID = process.env.TEST_CLOUDFLARE_ACCOUNT_ID;
25+
process.env.CLOUDFLARE_API_TOKEN = process.env.TEST_CLOUDFLARE_API_TOKEN;
26+
27+
const deployOut = execSync(
28+
`pnpm dlx wrangler deploy remote-worker.js --name ${remoteWorkerName} --compatibility-date 2025-06-19`,
29+
{
30+
stdio: "pipe",
31+
}
32+
);
33+
34+
if (
35+
!new RegExp(`Deployed\\s+${remoteWorkerName}\\b`).test(`${deployOut}`)
36+
) {
37+
throw new Error(`Failed to deploy ${remoteWorkerName}`);
38+
}
39+
40+
rmSync("./.tmp", { recursive: true, force: true });
41+
42+
mkdirSync("./.tmp");
43+
44+
writeFileSync(
45+
"./.tmp/wrangler.json",
46+
JSON.stringify(
47+
{
48+
name: "get-platform-proxy-fixture-test",
49+
compatibility_date: "2025-06-01",
50+
services: [
51+
{
52+
binding: "MY_WORKER",
53+
service: remoteWorkerName,
54+
experimental_remote: true,
55+
},
56+
],
57+
},
58+
undefined,
59+
2
60+
),
61+
"utf8"
62+
);
63+
});
64+
65+
test("getPlatformProxy works with remote bindings", async () => {
66+
const { env, dispose } = await getPlatformProxy({
67+
configPath: "./.tmp/wrangler.json",
68+
experimental: { remoteBindings: true },
69+
});
70+
71+
try {
72+
assert.strictEqual(
73+
await (await env.MY_WORKER.fetch("http://example.com")).text(),
74+
"Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!"
75+
);
76+
} finally {
77+
await dispose();
78+
}
79+
});
80+
81+
after(async () => {
82+
execSync(`pnpm dlx wrangler delete --name ${remoteWorkerName}`);
83+
rmSync("./.tmp", { recursive: true, force: true });
84+
});
85+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@fixture/get-platform-proxy-remote-bindings-node-test",
3+
"private": true,
4+
"description": "",
5+
"license": "ISC",
6+
"author": "",
7+
"type": "module",
8+
"scripts": {
9+
"test:ci": "node --test"
10+
},
11+
"devDependencies": {
12+
"@cloudflare/workers-tsconfig": "workspace:*",
13+
"@cloudflare/workers-types": "^4.20250617.0",
14+
"wrangler": "workspace:*"
15+
},
16+
"volta": {
17+
"extends": "../../package.json"
18+
}
19+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default {
2+
fetch() {
3+
return new Response(
4+
"Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!"
5+
);
6+
},
7+
};

packages/wrangler/src/api/integrations/platform/index.ts

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ import {
1111
buildMiniflareBindingOptions,
1212
buildSitesOptions,
1313
} from "../../../dev/miniflare";
14-
import { run } from "../../../experimental-flags";
1514
import { logger } from "../../../logger";
1615
import { getSiteAssetPaths } from "../../../sites";
1716
import { dedent } from "../../../utils/dedent";
17+
import { maybeStartOrUpdateRemoteProxySession } from "../../remoteBindings";
1818
import { CacheStorage } from "./caches";
1919
import { ExecutionContext } from "./executionContext";
2020
import { getServiceBindings } from "./services";
2121
import type { AssetsOptions } from "../../../assets";
2222
import type { Config, RawConfig, RawEnvironment } from "../../../config";
23+
import type { RemoteProxySession } from "../../remoteBindings";
2324
import type { IncomingRequestCfProperties } from "@cloudflare/workers-types/experimental";
2425
import type {
2526
MiniflareOptions,
@@ -58,6 +59,13 @@ export type GetPlatformProxyOptions = {
5859
* If `false` is specified no data is persisted on the filesystem.
5960
*/
6061
persist?: boolean | { path: string };
62+
/**
63+
* Experimental flags (note: these can change at any time and are not version-controlled use at your own risk)
64+
*/
65+
experimental?: {
66+
/** whether access to remove bindings should be enabled */
67+
remoteBindings?: boolean;
68+
};
6169
};
6270

6371
/**
@@ -103,29 +111,32 @@ export async function getPlatformProxy<
103111
>(
104112
options: GetPlatformProxyOptions = {}
105113
): Promise<PlatformProxy<Env, CfProperties>> {
114+
const experimentalRemoteBindings = !!options.experimental?.remoteBindings;
115+
106116
const env = options.environment;
107117

108-
const rawConfig = readConfig({
118+
const config = readConfig({
109119
config: options.configPath,
110120
env,
111121
});
112122

113-
const miniflareOptions = await run(
114-
{
115-
MULTIWORKER: false,
116-
RESOURCES_PROVISION: false,
117-
// TODO: when possible remote bindings should be made available for getPlatformProxy
118-
REMOTE_BINDINGS: false,
119-
},
120-
() => getMiniflareOptionsFromConfig(rawConfig, env, options)
121-
);
123+
let remoteProxySession: RemoteProxySession | undefined = undefined;
124+
if (experimentalRemoteBindings && config.configPath) {
125+
remoteProxySession = (
126+
(await maybeStartOrUpdateRemoteProxySession(config.configPath)) ?? {}
127+
).session;
128+
}
122129

123-
const mf = new Miniflare({
124-
script: "",
125-
modules: true,
126-
...(miniflareOptions as Record<string, unknown>),
130+
const miniflareOptions = await getMiniflareOptionsFromConfig({
131+
config,
132+
options,
133+
remoteProxyConnectionString:
134+
remoteProxySession?.remoteProxyConnectionString,
135+
remoteBindingsEnabled: experimentalRemoteBindings,
127136
});
128137

138+
const mf = new Miniflare(miniflareOptions);
139+
129140
const bindings: Env = await mf.getBindings();
130141

131142
const cf = await mf.getCf();
@@ -136,20 +147,40 @@ export async function getPlatformProxy<
136147
cf: cf as CfProperties,
137148
ctx: new ExecutionContext(),
138149
caches: new CacheStorage(),
139-
dispose: () => mf.dispose(),
150+
dispose: async () => {
151+
await remoteProxySession?.dispose();
152+
await mf.dispose();
153+
},
140154
};
141155
}
142156

143-
// this is only used by getPlatformProxy
144-
async function getMiniflareOptionsFromConfig(
145-
rawConfig: Config,
146-
env: string | undefined,
147-
options: GetPlatformProxyOptions
148-
): Promise<Partial<MiniflareOptions>> {
149-
const bindings = getBindings(rawConfig, env, true, {});
157+
/**
158+
* Builds an options configuration object for the `getPlatformProxy` functionality that
159+
* can be then passed to the Miniflare constructor
160+
*
161+
* @param args.config The wrangler configuration to base the options from
162+
* @param args.options The user provided `getPlatformProxy` options
163+
* @param args.remoteProxyConnectionString The potential remote proxy connection string to be used to connect the remote bindings
164+
* @param args.remoteBindingsEnabled Whether remote bindings are enabled
165+
* @returns an object ready to be passed to the Miniflare constructor
166+
*/
167+
async function getMiniflareOptionsFromConfig(args: {
168+
config: Config;
169+
options: GetPlatformProxyOptions;
170+
remoteProxyConnectionString?: RemoteProxyConnectionString;
171+
remoteBindingsEnabled: boolean;
172+
}): Promise<MiniflareOptions> {
173+
const {
174+
config,
175+
options,
176+
remoteProxyConnectionString,
177+
remoteBindingsEnabled,
178+
} = args;
179+
180+
const bindings = getBindings(config, options.environment, true, {});
150181

151-
if (rawConfig["durable_objects"]) {
152-
const { localBindings } = partitionDurableObjectBindings(rawConfig);
182+
if (config["durable_objects"]) {
183+
const { localBindings } = partitionDurableObjectBindings(config);
153184
if (localBindings.length > 0) {
154185
logger.warn(dedent`
155186
You have defined bindings to the following internal Durable Objects:
@@ -162,29 +193,29 @@ async function getMiniflareOptionsFromConfig(
162193
}
163194
}
164195
const workerDefinitions = await getBoundRegisteredWorkers({
165-
name: rawConfig.name,
196+
name: config.name,
166197
services: bindings.services,
167-
durableObjects: rawConfig["durable_objects"],
198+
durableObjects: config["durable_objects"],
168199
tailConsumers: [],
169200
});
170201

171202
const { bindingOptions, externalWorkers } = buildMiniflareBindingOptions(
172203
{
173-
name: rawConfig.name,
174-
complianceRegion: rawConfig.compliance_region,
204+
name: config.name,
205+
complianceRegion: config.compliance_region,
175206
bindings,
176207
workerDefinitions,
177208
queueConsumers: undefined,
178-
services: rawConfig.services,
209+
services: config.services,
179210
serviceBindings: {},
180-
migrations: rawConfig.migrations,
211+
migrations: config.migrations,
181212
imagesLocalMode: false,
182213
tails: [],
183214
containers: undefined,
184215
containerBuildId: undefined,
185216
},
186-
undefined,
187-
false
217+
remoteProxyConnectionString,
218+
remoteBindingsEnabled
188219
);
189220

190221
const defaultPersistRoot = getMiniflarePersistRoot(options.persist);
@@ -196,7 +227,7 @@ async function getMiniflareOptionsFromConfig(
196227
{
197228
script: "",
198229
modules: true,
199-
name: rawConfig.name,
230+
name: config.name,
200231
...bindingOptions,
201232
serviceBindings: {
202233
...serviceBindings,
@@ -208,7 +239,11 @@ async function getMiniflareOptionsFromConfig(
208239
defaultPersistRoot,
209240
};
210241

211-
return miniflareOptions;
242+
return {
243+
script: "",
244+
modules: true,
245+
...miniflareOptions,
246+
};
212247
}
213248

214249
/**

packages/wrangler/src/api/remoteBindings/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export function pickRemoteBindings(
106106
*
107107
* @param configPathOrWorkerConfig either a file path to a wrangler configuration file or an object containing the name of
108108
* the target worker alongside its bindings.
109-
* @param preExistingRemoteProxySessionData the data of a pre-existing remote proxy session if there was one null otherwise
109+
* @param preExistingRemoteProxySessionData the optional data of a pre-existing remote proxy session if there was one, this
110+
* argument can be omitted or set to null if there is no pre-existing remote proxy session
110111
* @returns null if no existing remote proxy session was provided and one should not be created (because the worker is not
111112
* defining any remote bindings), the data associated to the created/updated remote proxy session otherwise.
112113
*/
@@ -117,7 +118,7 @@ export async function maybeStartOrUpdateRemoteProxySession(
117118
name?: string;
118119
bindings: NonNullable<StartDevWorkerInput["bindings"]>;
119120
},
120-
preExistingRemoteProxySessionData: {
121+
preExistingRemoteProxySessionData?: {
121122
session: RemoteProxySession;
122123
remoteBindings: Record<string, Binding>;
123124
} | null

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)