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
5 changes: 5 additions & 0 deletions .changeset/beige-chicken-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": minor
---

Add Images binding (in private beta for the time being)
34 changes: 34 additions & 0 deletions packages/wrangler/e2e/dev-with-resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,40 @@ describe.sequential.each(RUNTIMES)("Bindings: $flags", ({ runtime, flags }) => {
await expect(res.text()).resolves.toBe("env.WORKFLOW is available");
});

describe.sequential.each([
{ imagesMode: "remote", extraFlags: "" },
{ imagesMode: "local", extraFlags: "--experimental-images-local-mode" },
] as const)("Images Binding Mode: $imagesMode", async ({ extraFlags }) => {
it("exposes Images bindings", async () => {
await helper.seed({
"wrangler.toml": dedent`
name = "my-images-demo"
main = "src/index.ts"
compatibility_date = "2024-12-27"

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

return new Response("env.IMAGES is available");
}
}
`,
});
const worker = helper.runLongLived(`wrangler dev ${flags} ${extraFlags}`);
const { url } = await worker.waitForReady();
const res = await fetch(url);

await expect(res.text()).resolves.toBe("env.IMAGES 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
1 change: 1 addition & 0 deletions packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"esbuild": "0.17.19",
"miniflare": "workspace:*",
"path-to-regexp": "6.3.0",
"sharp": "^0.33.5",
"unenv": "2.0.0-rc.1",
"workerd": "1.20250124.0"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/scripts/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const EXTERNAL_DEPENDENCIES = [

// workerd contains a native binary, so must be external. Wrangler depends on a pinned version.
"workerd",

// sharp contains native libraries
"sharp",
];

const pathToPackageJson = path.resolve(__dirname, "..", "package.json");
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 @@ -2302,6 +2302,69 @@ describe("normalizeAndValidateConfig()", () => {
});
});

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

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

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

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

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

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

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

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

// Worker Version Metadata
describe("[version_metadata]", () => {
it("should error if version_metadata is an array", () => {
Expand Down
31 changes: 31 additions & 0 deletions packages/wrangler/src/__tests__/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11903,6 +11903,37 @@ export default{
});
});

describe("images", () => {
it("should upload images bindings", async () => {
writeWranglerConfig({
images: { binding: "IMAGES_BIND" },
});
await fs.promises.writeFile("index.js", `export default {};`);
mockSubDomainRequest();
mockUploadWorkerRequest({
expectedBindings: [
{
type: "images",
name: "IMAGES_BIND",
},
],
});

await runWrangler("deploy index.js");
expect(std.out).toMatchInlineSnapshot(`
"Total Upload: xx KiB / gzip: xx KiB
Worker Startup Time: 100 ms
Your worker has access to the following bindings:
- Images:
- Name: IMAGES_BIND
Uploaded test-name (TIMINGS)
Deployed test-name triggers (TIMINGS)
https://test-name.test-sub-domain.workers.dev
Current Version ID: Galaxy-Class"
`);
});
});

describe("python", () => {
it("should upload python module defined in wrangler.toml", async () => {
writeWranglerConfig({
Expand Down
3 changes: 2 additions & 1 deletion packages/wrangler/src/__tests__/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,8 @@ describe.sequential("wrangler dev", () => {
--test-scheduled Test scheduled events by visiting /__scheduled in browser [boolean] [default: false]
--log-level Specify logging level [choices: \\"debug\\", \\"info\\", \\"log\\", \\"warn\\", \\"error\\", \\"none\\"] [default: \\"log\\"]
--show-interactive-dev-session Show interactive dev session (defaults to true if the terminal supports interactivity) [boolean]
--experimental-vectorize-bind-to-prod Bind to production Vectorize indexes in local development mode [boolean] [default: false]",
--experimental-vectorize-bind-to-prod Bind to production Vectorize indexes in local development mode [boolean] [default: false]
--experimental-images-local-mode Use a local lower-fidelity implementation of the Images binding [boolean] [default: false]",
"warn": "",
}
`);
Expand Down
3 changes: 2 additions & 1 deletion packages/wrangler/src/__tests__/pages/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ describe("pages", () => {
--persist-to Specify directory to use for local persistence (defaults to .wrangler/state) [string]
--log-level Specify logging level [choices: \\"debug\\", \\"info\\", \\"log\\", \\"warn\\", \\"error\\", \\"none\\"]
--show-interactive-dev-session Show interactive dev session (defaults to true if the terminal supports interactivity) [boolean]
--experimental-vectorize-bind-to-prod Bind to production Vectorize indexes in local development mode [boolean] [default: false]"
--experimental-vectorize-bind-to-prod Bind to production Vectorize indexes in local development mode [boolean] [default: false]
--experimental-images-local-mode Use a local lower-fidelity implementation of the Images binding [boolean] [default: false]"
`);
});

Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/src/__tests__/type-generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ const bindingsConfigMock: Omit<
ai: {
binding: "AI_BINDING",
},
images: {
binding: "IMAGES_BINDING",
},
version_metadata: {
binding: "VERSION_METADATA_BINDING",
},
Expand Down
3 changes: 3 additions & 0 deletions packages/wrangler/src/api/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface Unstable_DevOptions {
devEnv?: boolean;
fileBasedRegistry?: boolean;
vectorizeBindToProd?: boolean;
imagesLocalMode?: boolean;
enableIpc?: boolean;
};
}
Expand Down Expand Up @@ -126,6 +127,7 @@ export async function unstable_dev(
testMode,
testScheduled,
vectorizeBindToProd,
imagesLocalMode,
// 2. options for alpha/beta products/libs
d1Databases,
enablePagesAssetsServiceBinding,
Expand Down Expand Up @@ -218,6 +220,7 @@ export async function unstable_dev(
port: options?.port ?? 0,
experimentalProvision: undefined,
experimentalVectorizeBindToProd: vectorizeBindToProd ?? false,
experimentalImagesLocalMode: imagesLocalMode ?? false,
enableIpc: options?.experimental?.enableIpc,
};

Expand Down
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/integrations/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ async function getMiniflareOptionsFromConfig(
services: rawConfig.services,
serviceBindings: {},
migrations: rawConfig.migrations,
imagesLocalMode: false,
});

const persistOptions = getMiniflarePersistOptions(options.persist);
Expand Down Expand Up @@ -278,6 +279,7 @@ export function unstable_getMiniflareWorkerOptions(
services: [],
serviceBindings: {},
migrations: config.migrations,
imagesLocalMode: false,
});

// This function is currently only exported for the Workers Vitest pool.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ async function resolveDevConfig(
registry: input.dev?.registry,
bindVectorizeToProd: input.dev?.bindVectorizeToProd ?? false,
multiworkerPrimary: input.dev?.multiworkerPrimary,
imagesLocalMode: input.dev?.imagesLocalMode ?? false,
} satisfies StartDevWorkerOptions["dev"];
}

Expand Down Expand Up @@ -169,6 +170,7 @@ async function resolveBindings(
{
registry: input.dev?.registry,
local: !input.dev?.remote,
imagesLocalMode: input.dev?.imagesLocalMode,
name: config.name,
}
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export async function convertToConfigBundle(
services: bindings.services,
serviceBindings: fetchers,
bindVectorizeToProd: event.config.dev?.bindVectorizeToProd ?? false,
imagesLocalMode: event.config.dev?.imagesLocalMode ?? false,
testScheduled: !!event.config.dev.testScheduled,
};
}
Expand Down
4 changes: 4 additions & 0 deletions packages/wrangler/src/api/startDevWorker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ export interface StartDevWorkerInput {
/** Whether to use Vectorize mixed mode -- the worker is run locally but accesses to Vectorize are made remotely */
bindVectorizeToProd?: boolean;

/** Whether to use Images local mode -- this is lower fidelity, but doesn't require network access */
imagesLocalMode?: boolean;

/** Treat this as the primary worker in a multiworker setup (i.e. the first Worker in Miniflare's options) */
multiworkerPrimary?: boolean;
};
Expand Down Expand Up @@ -241,6 +244,7 @@ export type Binding =
| { type: "text_blob"; source: File }
| { type: "browser" }
| { type: "ai" }
| { type: "images" }
| { type: "version_metadata" }
| { type: "data_blob"; source: BinaryFile }
| ({ type: "durable_object_namespace" } & NameOmit<CfDurableObject>)
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 @@ -209,6 +209,11 @@ export function convertCfWorkerInitBindingstoBindings(
output[binding] = { type: "ai", ...x };
break;
}
case "images": {
const { binding, ...x } = info;
output[binding] = { type: "images", ...x };
break;
}
case "version_metadata": {
const { binding, ...x } = info;
output[binding] = { type: "version_metadata", ...x };
Expand Down Expand Up @@ -265,6 +270,7 @@ export async function convertBindingsToCfWorkerInitBindings(
text_blobs: undefined,
browser: undefined,
ai: undefined,
images: undefined,
version_metadata: undefined,
data_blobs: undefined,
durable_objects: undefined,
Expand Down Expand Up @@ -320,6 +326,8 @@ export async function convertBindingsToCfWorkerInitBindings(
bindings.browser = { binding: name };
} else if (binding.type === "ai") {
bindings.ai = { binding: name };
} else if (binding.type === "images") {
bindings.images = { binding: name };
} else if (binding.type === "version_metadata") {
bindings.version_metadata = { binding: name };
} else if (binding.type === "durable_object_namespace") {
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 @@ -340,6 +340,7 @@ export const defaultWranglerConfig: Config = {
services: [],
analytics_engine_datasets: [],
ai: undefined,
images: undefined,
version_metadata: undefined,

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

/**
* Binding to Cloudflare Images
*
* NOTE: This field is not automatically inherited from the top level environment,
* and so must be specified in every named environment.
*
* @default {}
* @nonInheritable
*/
images:
| {
binding: string;
}
| undefined;

/**
* Binding to the Worker Version's metadata
*/
Expand Down
14 changes: 12 additions & 2 deletions packages/wrangler/src/config/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1525,7 +1525,7 @@ function normalizeAndValidateEnvironment(
rawEnv,
envName,
"browser",
validateBrowserBinding(envName),
validateNamedSimpleBinding(envName),
undefined
),
ai: notInheritable(
Expand All @@ -1538,6 +1538,16 @@ function normalizeAndValidateEnvironment(
validateAIBinding(envName),
undefined
),
images: notInheritable(
diagnostics,
topLevelEnv,
rawConfig,
rawEnv,
envName,
"images",
validateNamedSimpleBinding(envName),
undefined
),
pipelines: notInheritable(
diagnostics,
topLevelEnv,
Expand Down Expand Up @@ -2243,7 +2253,7 @@ const validateAssetsConfig: ValidatorFn = (diagnostics, field, value) => {
return isValid;
};

const validateBrowserBinding =
const validateNamedSimpleBinding =
(envName: string): ValidatorFn =>
(diagnostics, field, value, config) => {
const fieldPath =
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 @@ -39,6 +39,7 @@ export function getBindings(
wasm_modules: options?.pages ? undefined : config?.wasm_modules,
browser: config?.browser,
ai: config?.ai,
images: config?.images,
version_metadata: config?.version_metadata,
text_blobs: options?.pages ? undefined : config?.text_blobs,
data_blobs: options?.pages ? undefined : config?.data_blobs,
Expand Down
Loading
Loading