diff --git a/.changeset/better-sheep-reply.md b/.changeset/better-sheep-reply.md new file mode 100644 index 000000000000..702b3842eca4 --- /dev/null +++ b/.changeset/better-sheep-reply.md @@ -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 +``` diff --git a/fixtures/get-platform-proxy-remote-bindings-node-test/index.test.js b/fixtures/get-platform-proxy-remote-bindings-node-test/index.test.js new file mode 100644 index 000000000000..ef3152dfedb2 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings-node-test/index.test.js @@ -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 }); + }); +}); diff --git a/fixtures/get-platform-proxy-remote-bindings-node-test/package.json b/fixtures/get-platform-proxy-remote-bindings-node-test/package.json new file mode 100644 index 000000000000..02f5ef743b83 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings-node-test/package.json @@ -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" + } +} diff --git a/fixtures/get-platform-proxy-remote-bindings-node-test/remote-worker.js b/fixtures/get-platform-proxy-remote-bindings-node-test/remote-worker.js new file mode 100644 index 000000000000..a88e54f0c7e0 --- /dev/null +++ b/fixtures/get-platform-proxy-remote-bindings-node-test/remote-worker.js @@ -0,0 +1,7 @@ +export default { + fetch() { + return new Response( + "Hello from a remote Worker part of the getPlatformProxy remote bindings fixture!" + ); + }, +}; diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index bce26b97bbc2..1f4ef6f2da87 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -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,32 @@ export async function getPlatformProxy< >( options: GetPlatformProxyOptions = {} ): Promise> { + 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 = ( + (await maybeStartOrUpdateRemoteProxySession(config.configPath)) ?? {} + ).session; + } - const mf = new Miniflare({ - script: "", - modules: true, - ...(miniflareOptions as Record), + 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(); @@ -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> { - 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 { + 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: @@ -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); @@ -196,7 +227,7 @@ async function getMiniflareOptionsFromConfig( { script: "", modules: true, - name: rawConfig.name, + name: config.name, ...bindingOptions, serviceBindings: { ...serviceBindings, @@ -208,7 +239,11 @@ async function getMiniflareOptionsFromConfig( defaultPersistRoot, }; - return miniflareOptions; + return { + script: "", + modules: true, + ...miniflareOptions, + }; } /** diff --git a/packages/wrangler/src/api/remoteBindings/index.ts b/packages/wrangler/src/api/remoteBindings/index.ts index 564ff1c4472a..9fa9b2748fbf 100644 --- a/packages/wrangler/src/api/remoteBindings/index.ts +++ b/packages/wrangler/src/api/remoteBindings/index.ts @@ -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. */ @@ -117,7 +118,7 @@ export async function maybeStartOrUpdateRemoteProxySession( name?: string; bindings: NonNullable; }, - preExistingRemoteProxySessionData: { + preExistingRemoteProxySessionData?: { session: RemoteProxySession; remoteBindings: Record; } | null diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98cf3a79e003..87d83266b633 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,18 @@ importers: specifier: workspace:* version: link:../../packages/wrangler + fixtures/get-platform-proxy-remote-bindings-node-test: + devDependencies: + '@cloudflare/workers-tsconfig': + specifier: workspace:* + version: link:../../packages/workers-tsconfig + '@cloudflare/workers-types': + specifier: ^4.20250617.0 + version: 4.20250617.0 + wrangler: + specifier: workspace:* + version: link:../../packages/wrangler + fixtures/import-npm: {} fixtures/import-wasm-example: