Skip to content

Commit

Permalink
Add support for Images binding (#7424)
Browse files Browse the repository at this point in the history
* Add Images binding

* Add Images remote preview mode

* Plumb images local mode flag through

* Add Images binding local mode

* Add Images E2E test

* Hoist @img packages

This fixes the fixture tests, perhaps because sharp does something
unusual with imports, see GH comment: nuxt/image#1210 (comment)

* Add local suffix when printing bindings

* Swap describe/it in E2E test

* Mark sharp as unbundled, rather than hoisting

* Remove zod

* Improve error messages
  • Loading branch information
ns476 authored Jan 23, 2025
1 parent f0f38b3 commit a7163b3
Show file tree
Hide file tree
Showing 30 changed files with 941 additions and 86 deletions.
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 @@ -691,6 +691,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.todo("exposes hyperdrive bindings");
it.skipIf(isLocal).todo("exposes send email 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.0",
"workerd": "1.20241230.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 @@ -11743,6 +11743,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 @@ -277,6 +278,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
2 changes: 2 additions & 0 deletions packages/wrangler/src/api/startDevWorker/ConfigController.ts
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 @@ -1512,7 +1512,7 @@ function normalizeAndValidateEnvironment(
rawEnv,
envName,
"browser",
validateBrowserBinding(envName),
validateNamedSimpleBinding(envName),
undefined
),
ai: notInheritable(
Expand All @@ -1525,6 +1525,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 @@ -2220,7 +2230,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

0 comments on commit a7163b3

Please sign in to comment.