Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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,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
// 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!"
);
},
};
93 changes: 66 additions & 27 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,33 @@ export async function getPlatformProxy<
>(
options: GetPlatformProxyOptions = {}
): Promise<PlatformProxy<Env, CfProperties>> {
const env = options.environment;
const experimentalRemoteBindings = !!options.experimental?.remoteBindings;

const targetEnvironment = options.environment;

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 =
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();
Expand All @@ -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
* @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;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit, but this destructing seems a bit messy to me.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 args around?)

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer the current version FWIW

Copy link
Contributor

Choose a reason for hiding this comment

The 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. remoteBindingsEnabled is temporary while we're in the experimental phase, and IMO 4 parameters is a perfectly reasonable numbr of parameters to a function. Making it an options object adds this destructuring, which in my mind just means that there's an extra 7 lines of boilerplate obscuring what this function does. If we want to keep it as an options object, I would find either 1) using args.remoteProxyConnectionString etc... consistently throughout the function body or 2) destructuring in the function definition much clearer to read.


const bindings = getBindings(rawConfig, targetEnvironment, true, {});

if (rawConfig["durable_objects"]) {
const { localBindings } = partitionDurableObjectBindings(rawConfig);
Expand Down Expand Up @@ -183,8 +218,8 @@ async function getMiniflareOptionsFromConfig(
containers: undefined,
containerBuildId: undefined,
},
undefined,
false
remoteProxyConnectionString,
remoteBindingsEnabled
);

const defaultPersistRoot = getMiniflarePersistRoot(options.persist);
Expand All @@ -208,7 +243,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