Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/better-sheep-reply.md
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,85 @@
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);
}

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
// 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",
}
);

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}`);
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!"
);
},
};
105 changes: 70 additions & 35 deletions packages/wrangler/src/api/integrations/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
};
};

/**
Expand Down Expand Up @@ -103,29 +111,32 @@ export async function getPlatformProxy<
>(
options: GetPlatformProxyOptions = {}
): Promise<PlatformProxy<Env, CfProperties>> {
const experimentalRemoteBindings = !!options.experimental?.remoteBindings;

const env = options.environment;

const rawConfig = readConfig({
const config = readConfig({
config: options.configPath,
env,
});

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 && config.configPath) {
remoteProxySession = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That look messy to me. IMO the former version looked better!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and by that I also mean that you do not have to implement all comments if they don't make sense to you - even my comments :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this wasn't quite what I was suggestion. I was thinking of (await maybeStartOrUpdateRemoteProxySession(config.configPath))?. session.

That said, this was mostly because the intermediate variable didn't add much clarity. What about:

({session: remoteProxySession } = await maybeStartOrUpdateRemoteProxySession(config.configPath)

(await maybeStartOrUpdateRemoteProxySession(config.configPath)) ?? {}
).session;
}

const mf = new Miniflare({
script: "",
modules: true,
...(miniflareOptions as Record<string, unknown>),
const miniflareOptions = await getMiniflareOptionsFromConfig({
config,
options,
remoteProxyConnectionString:
remoteProxySession?.remoteProxyConnectionString,
remoteBindingsEnabled: experimentalRemoteBindings,
});

const mf = new Miniflare(miniflareOptions);

const bindings: Env = await mf.getBindings();

const cf = await mf.getCf();
Expand All @@ -136,20 +147,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.config The wrangler configuration to base the options from
* @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: {
config: Config;
options: GetPlatformProxyOptions;
remoteProxyConnectionString?: RemoteProxyConnectionString;
remoteBindingsEnabled: boolean;
}): Promise<MiniflareOptions> {
const {
config,
options,
remoteProxyConnectionString,
remoteBindingsEnabled,
} = args;

const bindings = getBindings(config, options.environment, true, {});

if (rawConfig["durable_objects"]) {
const { localBindings } = partitionDurableObjectBindings(rawConfig);
if (config["durable_objects"]) {
const { localBindings } = partitionDurableObjectBindings(config);
if (localBindings.length > 0) {
logger.warn(dedent`
You have defined bindings to the following internal Durable Objects:
Expand All @@ -162,29 +193,29 @@ async function getMiniflareOptionsFromConfig(
}
}
const workerDefinitions = await getBoundRegisteredWorkers({
name: rawConfig.name,
name: config.name,
services: bindings.services,
durableObjects: rawConfig["durable_objects"],
durableObjects: config["durable_objects"],
tailConsumers: [],
});

const { bindingOptions, externalWorkers } = buildMiniflareBindingOptions(
{
name: rawConfig.name,
complianceRegion: rawConfig.compliance_region,
name: config.name,
complianceRegion: config.compliance_region,
bindings,
workerDefinitions,
queueConsumers: undefined,
services: rawConfig.services,
services: config.services,
serviceBindings: {},
migrations: rawConfig.migrations,
migrations: config.migrations,
imagesLocalMode: false,
tails: [],
containers: undefined,
containerBuildId: undefined,
},
undefined,
false
remoteProxyConnectionString,
remoteBindingsEnabled
);

const defaultPersistRoot = getMiniflarePersistRoot(options.persist);
Expand All @@ -196,7 +227,7 @@ async function getMiniflareOptionsFromConfig(
{
script: "",
modules: true,
name: rawConfig.name,
name: config.name,
...bindingOptions,
serviceBindings: {
...serviceBindings,
Expand All @@ -208,7 +239,11 @@ async function getMiniflareOptionsFromConfig(
defaultPersistRoot,
};

return miniflareOptions;
return {
script: "",
modules: true,
...miniflareOptions,
};
}

/**
Expand Down
5 changes: 3 additions & 2 deletions packages/wrangler/src/api/remoteBindings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export function pickRemoteBindings(
*
* @param configPathOrWorkerConfig either a file path to a wrangler configuration file or an object containing the name of
* the target worker alongside its bindings.
* @param preExistingRemoteProxySessionData the data of a pre-existing remote proxy session if there was one null otherwise
* @param preExistingRemoteProxySessionData the optional data of a pre-existing remote proxy session if there was one, this
* argument can be omitted or set to null if there is no pre-existing remote proxy session
* @returns null if no existing remote proxy session was provided and one should not be created (because the worker is not
* defining any remote bindings), the data associated to the created/updated remote proxy session otherwise.
*/
Expand All @@ -117,7 +118,7 @@ export async function maybeStartOrUpdateRemoteProxySession(
name?: string;
bindings: NonNullable<StartDevWorkerInput["bindings"]>;
},
preExistingRemoteProxySessionData: {
preExistingRemoteProxySessionData?: {
session: RemoteProxySession;
remoteBindings: Record<string, Binding>;
} | null
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading