Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changeset/brown-pans-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"miniflare": patch
"wrangler": patch
---

Add media binding support
6 changes: 5 additions & 1 deletion packages/miniflare/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { HELLO_WORLD_PLUGIN, HELLO_WORLD_PLUGIN_NAME } from "./hello-world";
import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive";
import { IMAGES_PLUGIN, IMAGES_PLUGIN_NAME } from "./images";
import { KV_PLUGIN, KV_PLUGIN_NAME } from "./kv";
import { MEDIA_PLUGIN, MEDIA_PLUGIN_NAME } from "./media";
import { MTLS_PLUGIN, MTLS_PLUGIN_NAME } from "./mtls";
import { PIPELINE_PLUGIN, PIPELINES_PLUGIN_NAME } from "./pipelines";
import { QUEUES_PLUGIN, QUEUES_PLUGIN_NAME } from "./queues";
Expand Down Expand Up @@ -61,6 +62,7 @@ export const PLUGINS = {
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
[HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN,
[WORKER_LOADER_PLUGIN_NAME]: WORKER_LOADER_PLUGIN,
[MEDIA_PLUGIN_NAME]: MEDIA_PLUGIN,
};
export type Plugins = typeof PLUGINS;

Expand Down Expand Up @@ -121,7 +123,8 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
z.input<typeof VECTORIZE_PLUGIN.options> &
z.input<typeof MTLS_PLUGIN.options> &
z.input<typeof HELLO_WORLD_PLUGIN.options> &
z.input<typeof WORKER_LOADER_PLUGIN.options>;
z.input<typeof WORKER_LOADER_PLUGIN.options> &
z.input<typeof MEDIA_PLUGIN.options>;

export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
z.input<typeof CACHE_PLUGIN.sharedOptions> &
Expand Down Expand Up @@ -193,3 +196,4 @@ export * from "./vectorize";
export * from "./mtls";
export * from "./hello-world";
export * from "./worker-loader";
export * from "./media";
74 changes: 74 additions & 0 deletions packages/miniflare/src/plugins/media/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from "node:assert";
import { z } from "zod";
import {
getUserBindingServiceName,
Plugin,
ProxyNodeBinding,
remoteProxyClientWorker,
RemoteProxyConnectionString,
} from "../shared";

export const MEDIA_PLUGIN_NAME = "media";

const MediaSchema = z.object({
binding: z.string(),
remoteProxyConnectionString: z.custom<RemoteProxyConnectionString>(),
});

export const MediaOptionsSchema = z.object({
media: MediaSchema.optional(),
});

export const MEDIA_PLUGIN: Plugin<typeof MediaOptionsSchema> = {
options: MediaOptionsSchema,
async getBindings(options) {
if (!options.media) {
return [];
}

assert(
options.media.remoteProxyConnectionString,
"Media binding only supports Mixed Mode"
);

return [
{
name: options.media.binding,
service: {
name: getUserBindingServiceName(
MEDIA_PLUGIN_NAME,
options.media.binding,
options.media.remoteProxyConnectionString
),
},
},
];
},
getNodeBindings(options: z.infer<typeof MediaOptionsSchema>) {
if (!options.media) {
return {};
}
return {
[options.media.binding]: new ProxyNodeBinding(),
};
},
async getServices({ options }) {
if (!options.media) {
return [];
}

return [
{
name: getUserBindingServiceName(
MEDIA_PLUGIN_NAME,
options.media.binding,
options.media.remoteProxyConnectionString
),
worker: remoteProxyClientWorker(
options.media.remoteProxyConnectionString,
options.media.binding
),
},
];
},
};
30 changes: 30 additions & 0 deletions packages/wrangler/e2e/dev-with-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,36 @@ describe.sequential.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => {
});
});

it.skipIf(!CLOUDFLARE_ACCOUNT_ID)("exposes Media bindings", async () => {
await helper.seed({
"wrangler.toml": dedent`
name = "my-media-demo"
main = "src/index.ts"
compatibility_date = "2025-09-06"

[media]
binding = "MEDIA"
experimental_remote = true
`,
"src/index.ts": dedent`
export default {
async fetch(request, env, ctx) {
if (env.MEDIA === undefined) {
return new Response("env.MEDIA is undefined");
}

return new Response("env.MEDIA is available");
}
}
`,
});
const worker = helper.runLongLived(`wrangler dev --x-remote-bindings`);
const { url } = await worker.waitForReady();
const res = await fetch(url);

await expect(res.text()).resolves.toBe("env.MEDIA is available");
});

// TODO(soon): implement E2E tests for other bindings
it.skipIf(isLocal).todo("exposes send email bindings");
it.skipIf(isLocal).todo("exposes browser bindings");
Expand Down
63 changes: 63 additions & 0 deletions packages/wrangler/src/__tests__/config/configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,69 @@ describe("normalizeAndValidateConfig()", () => {
});
});

// Media
describe("[media]", () => {
it("should error if media is an array", () => {
const { diagnostics } = normalizeAndValidateConfig(
{ media: [] } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"media\\" should be an object but got []."
`);
});

it("should error if media is a string", () => {
const { diagnostics } = normalizeAndValidateConfig(
{ media: "BAD" } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"media\\" should be an object but got \\"BAD\\"."
`);
});

it("should error if media is a number", () => {
const { diagnostics } = normalizeAndValidateConfig(
{ media: 999 } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"media\\" should be an object but got 999."
`);
});

it("should error if media is null", () => {
const { diagnostics } = normalizeAndValidateConfig(
{ media: null } as unknown as RawConfig,
undefined,
undefined,
{ env: undefined }
);

expect(diagnostics.hasWarnings()).toBe(false);
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
"Processing wrangler configuration:
- The field \\"media\\" should be an object but got null."
`);
});
});

// Worker Version Metadata
describe("[version_metadata]", () => {
it("should error if version_metadata is an array", () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,9 @@ const bindingsConfigMock: Omit<
images: {
binding: "IMAGES_BINDING",
},
media: {
binding: "MEDIA_BINDING",
},
version_metadata: {
binding: "VERSION_METADATA_BINDING",
},
Expand Down Expand Up @@ -469,6 +472,7 @@ describe("generate types", () => {
BROWSER_BINDING: Fetcher;
AI_BINDING: Ai;
IMAGES_BINDING: ImagesBinding;
MEDIA_BINDING: MediaBinding;
VERSION_METADATA_BINDING: WorkerVersionMetadata;
ASSETS_BINDING: Fetcher;
PIPELINE: import(\\"cloudflare:pipelines\\").Pipeline<import(\\"cloudflare:pipelines\\").PipelineRecord>;
Expand Down Expand Up @@ -563,6 +567,7 @@ describe("generate types", () => {
BROWSER_BINDING: Fetcher;
AI_BINDING: Ai;
IMAGES_BINDING: ImagesBinding;
MEDIA_BINDING: MediaBinding;
VERSION_METADATA_BINDING: WorkerVersionMetadata;
ASSETS_BINDING: Fetcher;
PIPELINE: import(\\"cloudflare:pipelines\\").Pipeline<import(\\"cloudflare:pipelines\\").PipelineRecord>;
Expand Down Expand Up @@ -721,6 +726,7 @@ describe("generate types", () => {
BROWSER_BINDING: Fetcher;
AI_BINDING: Ai;
IMAGES_BINDING: ImagesBinding;
MEDIA_BINDING: MediaBinding;
VERSION_METADATA_BINDING: WorkerVersionMetadata;
ASSETS_BINDING: Fetcher;
PIPELINE: import(\\"cloudflare:pipelines\\").Pipeline<import(\\"cloudflare:pipelines\\").PipelineRecord>;
Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/startDevWorker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
CfHyperdrive,
CfKvNamespace,
CfLogfwdrBinding,
CfMediaBinding,
CfModule,
CfMTlsCertificate,
CfPipeline,
Expand Down Expand Up @@ -302,6 +303,7 @@ export type Binding =
| ({ type: "secrets_store_secret" } & BindingOmit<CfSecretsStoreSecrets>)
| ({ type: "logfwdr" } & NameOmit<CfLogfwdrBinding>)
| ({ type: "unsafe_hello_world" } & BindingOmit<CfHelloWorld>)
| ({ type: "media" } & BindingOmit<CfMediaBinding>)
| { type: `unsafe_${string}` }
| { type: "assets" };

Expand Down
8 changes: 8 additions & 0 deletions packages/wrangler/src/api/startDevWorker/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,11 @@ export function convertCfWorkerInitBindingsToBindings(
}
break;
}
case "media": {
const { binding, ...x } = info;
output[binding] = { type: "media", ...x };
break;
}
default: {
assertNever(type);
}
Expand Down Expand Up @@ -324,6 +329,7 @@ export async function convertBindingsToCfWorkerInitBindings(
assets: undefined,
pipelines: undefined,
unsafe_hello_world: undefined,
media: undefined,
};

const fetchers: Record<string, ServiceFetch> = {};
Expand Down Expand Up @@ -413,6 +419,8 @@ export async function convertBindingsToCfWorkerInitBindings(
} else if (binding.type === "unsafe_hello_world") {
bindings.unsafe_hello_world ??= [];
bindings.unsafe_hello_world.push({ ...binding, binding: name });
} else if (binding.type === "media") {
bindings.media = { ...binding, binding: name };
} else if (isUnsafeBindingType(binding.type)) {
bindings.unsafe ??= {
bindings: [],
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ export const defaultWranglerConfig: Config = {
analytics_engine_datasets: [],
ai: undefined,
images: undefined,
media: undefined,
version_metadata: undefined,
unsafe_hello_world: [],

Expand Down
17 changes: 17 additions & 0 deletions packages/wrangler/src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,23 @@ export interface EnvironmentNonInheritable {
}
| undefined;

/**
* Binding to Cloudflare Media Transformations
*
* NOTE: This field is not automatically inherited from the top level environment,
* and so must be specified in every named environment.
*
* @default {}
* @nonInheritable
*/
media:
| {
binding: string;
/** Whether the Media binding should be remote or not (only available under `--x-remote-bindings`) */
experimental_remote?: boolean;
}
| undefined;

/**
* Binding to the Worker Version's metadata
*/
Expand Down
11 changes: 11 additions & 0 deletions packages/wrangler/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1402,6 +1402,16 @@ function normalizeAndValidateEnvironment(
validateNamedSimpleBinding(envName),
undefined
),
media: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"media",
validateNamedSimpleBinding(envName),
undefined
),
pipelines: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -2353,6 +2363,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
"logfwdr",
"mtls_certificate",
"pipeline",
"media",
];

if (safeBindings.includes(value.type)) {
Expand Down
1 change: 1 addition & 0 deletions packages/wrangler/src/deployment-bundle/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function getBindings(
capnp: config?.unsafe.capnp,
},
unsafe_hello_world: options?.pages ? undefined : config?.unsafe_hello_world,
media: config?.media,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type WorkerMetadataBinding =
| { type: "version_metadata"; name: string }
| { type: "data_blob"; name: string; part: string }
| { type: "kv_namespace"; name: string; namespace_id: string; raw?: boolean }
| { type: "media"; name: string }
| {
type: "send_email";
name: string;
Expand Down Expand Up @@ -530,6 +531,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData {
});
}

if (bindings.media !== undefined) {
metadataBindings.push({
name: bindings.media.binding,
type: "media",
});
}

if (bindings.version_metadata !== undefined) {
metadataBindings.push({
name: bindings.version_metadata.binding,
Expand Down
Loading