Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions .changeset/better-sheep-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

add remote bindings support to `getPlatformProxy`
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!"
);
},
};
49 changes: 35 additions & 14 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,27 +111,35 @@ export async function getPlatformProxy<
>(
options: GetPlatformProxyOptions = {}
): Promise<PlatformProxy<Env, CfProperties>> {
const experimentalRemoteBindings = !!options.experimental?.remoteBindings;

const env = options.environment;

const rawConfig = 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 && rawConfig.configPath) {
const maybeRemoteProxySessionWrap =
await maybeStartOrUpdateRemoteProxySession(rawConfig.configPath);
remoteProxySession = maybeRemoteProxySessionWrap?.session;
}

const miniflareOptions = await getMiniflareOptionsFromConfig(
rawConfig,
env,
options,
remoteProxySession?.remoteProxyConnectionString,
experimentalRemoteBindings
);

const mf = new Miniflare({
script: "",
// @ts-expect-error TS doesn't like the spreading of miniflareConfig
modules: true,
...(miniflareOptions as Record<string, unknown>),
...miniflareOptions,
});

const bindings: Env = await mf.getBindings();
Expand All @@ -136,15 +152,20 @@ 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
options: GetPlatformProxyOptions,
remoteProxyConnectionString?: RemoteProxyConnectionString,
remoteBindingsEnabled = false
): Promise<Partial<MiniflareOptions>> {
const bindings = getBindings(rawConfig, env, true, {});

Expand Down Expand Up @@ -183,8 +204,8 @@ async function getMiniflareOptionsFromConfig(
containers: undefined,
containerBuildId: undefined,
},
undefined,
false
remoteProxyConnectionString,
remoteBindingsEnabled
);

const defaultPersistRoot = getMiniflarePersistRoot(options.persist);
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