From 9d08af81893df499d914b890d784a9554ebf9507 Mon Sep 17 00:00:00 2001 From: Nicholas Skehin Date: Wed, 5 Feb 2025 11:12:01 +0000 Subject: [PATCH] Add support for Images bindings, with dynamic Sharp import (#8008) * Add support for Images binding * 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: https://github.com/nuxt/image/issues/1210#issuecomment-1904087632 * Add local suffix when printing bindings * Swap describe/it in E2E test * Mark sharp as unbundled, rather than hoisting * Remove zod * Improve error messages * Import sharp dynamically We want everything that doesn't depend on sharp to work if it is unavailable, so we need to import it dynamically. * Update changeset * Patch not minor changeset Co-authored-by: Edmund Hung * Improve messages * Use CommonJS for Wrangler tests --------- Co-authored-by: Edmund Hung --- .changeset/ninety-donuts-warn.md | 5 + .../wrangler/e2e/dev-with-resources.test.ts | 34 ++ packages/wrangler/package.json | 3 +- packages/wrangler/scripts/deps.ts | 3 + .../__tests__/config/configuration.test.ts | 63 ++++ .../wrangler/src/__tests__/deploy.test.ts | 31 ++ packages/wrangler/src/__tests__/dev.test.ts | 3 +- .../src/__tests__/pages/pages.test.ts | 3 +- packages/wrangler/src/__tests__/tsconfig.json | 1 + .../src/__tests__/type-generation.test.ts | 3 + packages/wrangler/src/api/dev.ts | 3 + .../src/api/integrations/platform/index.ts | 2 + .../api/startDevWorker/ConfigController.ts | 2 + .../startDevWorker/LocalRuntimeController.ts | 1 + .../wrangler/src/api/startDevWorker/types.ts | 4 + .../wrangler/src/api/startDevWorker/utils.ts | 8 + packages/wrangler/src/config/config.ts | 1 + packages/wrangler/src/config/environment.ts | 15 + packages/wrangler/src/config/validation.ts | 14 +- .../src/deployment-bundle/bindings.ts | 1 + .../create-worker-upload-form.ts | 8 + .../wrangler/src/deployment-bundle/worker.ts | 8 + packages/wrangler/src/dev.ts | 9 + packages/wrangler/src/dev/miniflare.ts | 50 ++- packages/wrangler/src/images/fetcher.ts | 35 ++ packages/wrangler/src/images/local.ts | 221 ++++++++++++ packages/wrangler/src/init.ts | 7 + packages/wrangler/src/pages/dev.ts | 7 + packages/wrangler/src/secret/index.ts | 1 + packages/wrangler/src/utils/print-bindings.ts | 15 + pnpm-lock.yaml | 327 ++++++++++++++++-- 31 files changed, 853 insertions(+), 35 deletions(-) create mode 100644 .changeset/ninety-donuts-warn.md create mode 100644 packages/wrangler/src/images/fetcher.ts create mode 100644 packages/wrangler/src/images/local.ts diff --git a/.changeset/ninety-donuts-warn.md b/.changeset/ninety-donuts-warn.md new file mode 100644 index 000000000000..7809c83165c8 --- /dev/null +++ b/.changeset/ninety-donuts-warn.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +Add support for Images bindings (in private beta for now), with optional local support for platforms where Sharp is available. diff --git a/packages/wrangler/e2e/dev-with-resources.test.ts b/packages/wrangler/e2e/dev-with-resources.test.ts index ede124475acd..648583f801bd 100644 --- a/packages/wrangler/e2e/dev-with-resources.test.ts +++ b/packages/wrangler/e2e/dev-with-resources.test.ts @@ -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"); diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 479da41f4ebb..f8040d8e36a3 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -171,7 +171,8 @@ } }, "optionalDependencies": { - "fsevents": "~2.3.2" + "fsevents": "~2.3.2", + "sharp": "^0.33.5" }, "engines": { "node": ">=16.17.0" diff --git a/packages/wrangler/scripts/deps.ts b/packages/wrangler/scripts/deps.ts index b0d7db455b7a..1019e0d2253a 100644 --- a/packages/wrangler/scripts/deps.ts +++ b/packages/wrangler/scripts/deps.ts @@ -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"); diff --git a/packages/wrangler/src/__tests__/config/configuration.test.ts b/packages/wrangler/src/__tests__/config/configuration.test.ts index 563c999f8bee..06c2af7bb33e 100644 --- a/packages/wrangler/src/__tests__/config/configuration.test.ts +++ b/packages/wrangler/src/__tests__/config/configuration.test.ts @@ -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", () => { diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index d202a395b7ab..d2280d400881 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -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({ diff --git a/packages/wrangler/src/__tests__/dev.test.ts b/packages/wrangler/src/__tests__/dev.test.ts index 5cbce0f5ecb1..00560b045321 100644 --- a/packages/wrangler/src/__tests__/dev.test.ts +++ b/packages/wrangler/src/__tests__/dev.test.ts @@ -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": "", } `); diff --git a/packages/wrangler/src/__tests__/pages/pages.test.ts b/packages/wrangler/src/__tests__/pages/pages.test.ts index f8fedd5add50..e8bdb2cdeeb3 100644 --- a/packages/wrangler/src/__tests__/pages/pages.test.ts +++ b/packages/wrangler/src/__tests__/pages/pages.test.ts @@ -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]" `); }); diff --git a/packages/wrangler/src/__tests__/tsconfig.json b/packages/wrangler/src/__tests__/tsconfig.json index 4eba0fc856e8..b04badb6b553 100644 --- a/packages/wrangler/src/__tests__/tsconfig.json +++ b/packages/wrangler/src/__tests__/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "@cloudflare/workers-tsconfig/tsconfig.json", "compilerOptions": { + "module": "CommonJS", "types": ["node", "vitest/globals"], "jsx": "preserve" }, diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 45385df0c988..37848d761787 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -163,6 +163,9 @@ const bindingsConfigMock: Omit< ai: { binding: "AI_BINDING", }, + images: { + binding: "IMAGES_BINDING", + }, version_metadata: { binding: "VERSION_METADATA_BINDING", }, diff --git a/packages/wrangler/src/api/dev.ts b/packages/wrangler/src/api/dev.ts index 023d746598a9..407fd113a4a1 100644 --- a/packages/wrangler/src/api/dev.ts +++ b/packages/wrangler/src/api/dev.ts @@ -82,6 +82,7 @@ export interface Unstable_DevOptions { devEnv?: boolean; fileBasedRegistry?: boolean; vectorizeBindToProd?: boolean; + imagesLocalMode?: boolean; enableIpc?: boolean; }; } @@ -126,6 +127,7 @@ export async function unstable_dev( testMode, testScheduled, vectorizeBindToProd, + imagesLocalMode, // 2. options for alpha/beta products/libs d1Databases, enablePagesAssetsServiceBinding, @@ -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, }; diff --git a/packages/wrangler/src/api/integrations/platform/index.ts b/packages/wrangler/src/api/integrations/platform/index.ts index 7f77de04a687..b5c5f1acca36 100644 --- a/packages/wrangler/src/api/integrations/platform/index.ts +++ b/packages/wrangler/src/api/integrations/platform/index.ts @@ -150,6 +150,7 @@ async function getMiniflareOptionsFromConfig( services: rawConfig.services, serviceBindings: {}, migrations: rawConfig.migrations, + imagesLocalMode: false, }); const persistOptions = getMiniflarePersistOptions(options.persist); @@ -272,6 +273,7 @@ export function unstable_getMiniflareWorkerOptions( services: [], serviceBindings: {}, migrations: config.migrations, + imagesLocalMode: false, }); // This function is currently only exported for the Workers Vitest pool. diff --git a/packages/wrangler/src/api/startDevWorker/ConfigController.ts b/packages/wrangler/src/api/startDevWorker/ConfigController.ts index c95f422a3c05..69c3d9ddff5c 100644 --- a/packages/wrangler/src/api/startDevWorker/ConfigController.ts +++ b/packages/wrangler/src/api/startDevWorker/ConfigController.ts @@ -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"]; } @@ -169,6 +170,7 @@ async function resolveBindings( { registry: input.dev?.registry, local: !input.dev?.remote, + imagesLocalMode: input.dev?.imagesLocalMode, name: config.name, } ); diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 05fb3bb0a686..cacb947554fb 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -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, }; } diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index 2b5575f8c386..ca071300d943 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -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; }; @@ -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) diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index ebb7e16e4cfd..a9a82e17282f 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -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 }; @@ -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, @@ -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") { diff --git a/packages/wrangler/src/config/config.ts b/packages/wrangler/src/config/config.ts index 76212c6ff8cb..3eaab8a1791a 100644 --- a/packages/wrangler/src/config/config.ts +++ b/packages/wrangler/src/config/config.ts @@ -340,6 +340,7 @@ export const defaultWranglerConfig: Config = { services: [], analytics_engine_datasets: [], ai: undefined, + images: undefined, version_metadata: undefined, /*====================================================*/ diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 94106cc8f40e..1d49038ca279 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -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 */ diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index e6dd92b8f09d..5dba0d2ce33b 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -1530,7 +1530,7 @@ function normalizeAndValidateEnvironment( rawEnv, envName, "browser", - validateBrowserBinding(envName), + validateNamedSimpleBinding(envName), undefined ), ai: notInheritable( @@ -1543,6 +1543,16 @@ function normalizeAndValidateEnvironment( validateAIBinding(envName), undefined ), + images: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "images", + validateNamedSimpleBinding(envName), + undefined + ), pipelines: notInheritable( diagnostics, topLevelEnv, @@ -2248,7 +2258,7 @@ const validateAssetsConfig: ValidatorFn = (diagnostics, field, value) => { return isValid; }; -const validateBrowserBinding = +const validateNamedSimpleBinding = (envName: string): ValidatorFn => (diagnostics, field, value, config) => { const fieldPath = diff --git a/packages/wrangler/src/deployment-bundle/bindings.ts b/packages/wrangler/src/deployment-bundle/bindings.ts index ddaf785f6e09..22b40123057d 100644 --- a/packages/wrangler/src/deployment-bundle/bindings.ts +++ b/packages/wrangler/src/deployment-bundle/bindings.ts @@ -38,6 +38,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, diff --git a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts index e440ff8a7b0f..2f732851f3e4 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -62,6 +62,7 @@ export type WorkerMetadataBinding = | { type: "text_blob"; name: string; part: string } | { type: "browser"; name: string } | { type: "ai"; name: string; staging?: boolean } + | { type: "images"; name: string } | { type: "version_metadata"; name: string } | { type: "data_blob"; name: string; part: string } | { type: "kv_namespace"; name: string; namespace_id: string } @@ -451,6 +452,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { }); } + if (bindings.images !== undefined) { + metadataBindings.push({ + name: bindings.images.binding, + type: "images", + }); + } + if (bindings.version_metadata !== undefined) { metadataBindings.push({ name: bindings.version_metadata.binding, diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index 9622b838a6ef..1200a1e5cbe1 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -127,6 +127,13 @@ export interface CfAIBinding { staging?: boolean; } +/** + * A binding to Cloudflare Images + */ +export interface CfImagesBinding { + binding: string; +} + /** * A binding to the Worker Version's metadata */ @@ -328,6 +335,7 @@ export interface CfWorkerInit { text_blobs: CfTextBlobBindings | undefined; browser: CfBrowserBinding | undefined; ai: CfAIBinding | undefined; + images: CfImagesBinding | undefined; version_metadata: CfVersionMetadataBinding | undefined; data_blobs: CfDataBlobBindings | undefined; durable_objects: { bindings: CfDurableObject[] } | undefined; diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index af097dc9422b..c94dca7369cd 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -324,6 +324,12 @@ export const dev = createCommand({ "Bind to production Vectorize indexes in local development mode", default: false, }, + "experimental-images-local-mode": { + type: "boolean", + describe: + "Use a local lower-fidelity implementation of the Images binding", + default: false, + }, }, async validateArgs(args) { if (args.liveReload && args.remote) { @@ -551,6 +557,7 @@ async function setupDevEnv( text_blobs: undefined, browser: undefined, ai: args.ai, + images: undefined, version_metadata: args.version_metadata, data_blobs: undefined, durable_objects: { bindings: args.durableObjects ?? [] }, @@ -601,6 +608,7 @@ async function setupDevEnv( ? null : devEnv.config.latestConfig?.dev.registry, bindVectorizeToProd: args.experimentalVectorizeBindToProd, + imagesLocalMode: args.experimentalImagesLocalMode, multiworkerPrimary: args.multiworkerPrimary, }, legacy: { @@ -1084,6 +1092,7 @@ export function getBindings( analytics_engine_datasets: configParam.analytics_engine_datasets, browser: configParam.browser, ai: args.ai || configParam.ai, + images: configParam.images, version_metadata: args.version_metadata || configParam.version_metadata, unsafe: { bindings: configParam.unsafe.bindings, diff --git a/packages/wrangler/src/dev/miniflare.ts b/packages/wrangler/src/dev/miniflare.ts index 42298aa7b4d2..3aabfe5d6905 100644 --- a/packages/wrangler/src/dev/miniflare.ts +++ b/packages/wrangler/src/dev/miniflare.ts @@ -9,7 +9,13 @@ import { } from "../ai/fetcher"; import { ModuleTypeToRuleType } from "../deployment-bundle/module-collection"; import { withSourceURLs } from "../deployment-bundle/source-url"; -import { UserError } from "../errors"; +import { createFatalError, UserError } from "../errors"; +import { + EXTERNAL_IMAGES_WORKER_NAME, + EXTERNAL_IMAGES_WORKER_SCRIPT, + imagesLocalFetcher, + imagesRemoteFetcher, +} from "../images/fetcher"; import { logger } from "../logger"; import { getSourceMappedString } from "../sourcemap"; import { updateCheck } from "../update-check"; @@ -187,6 +193,7 @@ export interface ConfigBundle { services: Config["services"] | undefined; serviceBindings: Record; bindVectorizeToProd: boolean; + imagesLocalMode: boolean; testScheduled: boolean; } @@ -384,6 +391,7 @@ type MiniflareBindingsConfig = Pick< | "name" | "services" | "serviceBindings" + | "imagesLocalMode" > & Partial>; @@ -607,6 +615,28 @@ export function buildMiniflareBindingOptions(config: MiniflareBindingsConfig): { }; } + if (bindings.images?.binding) { + externalWorkers.push({ + name: EXTERNAL_IMAGES_WORKER_NAME, + modules: [ + { + type: "ESModule", + path: "index.mjs", + contents: EXTERNAL_IMAGES_WORKER_SCRIPT, + }, + ], + serviceBindings: { + FETCHER: config.imagesLocalMode + ? imagesLocalFetcher + : imagesRemoteFetcher, + }, + }); + + wrappedBindings[bindings.images?.binding] = { + scriptName: EXTERNAL_IMAGES_WORKER_NAME, + }; + } + if (bindings.vectorize) { for (const vectorizeBinding of bindings.vectorize) { const bindingName = vectorizeBinding.binding; @@ -906,6 +936,7 @@ export function handleRuntimeStdio(stdout: Readable, stderr: Readable) { let didWarnMiniflareCronSupport = false; let didWarnMiniflareVectorizeSupport = false; let didWarnAiAccountUsage = false; +let didWarnImagesLocalModeUsage = false; export type Options = Extract; @@ -952,6 +983,23 @@ export async function buildMiniflareOptions( } } + if (config.bindings.images && config.imagesLocalMode) { + if (!didWarnImagesLocalModeUsage) { + try { + await import("sharp"); + } catch { + const msg = + "Sharp must be installed to use the Images binding local mode; check your version of Node is compatible"; + throw createFatalError(msg, false); + } + + didWarnImagesLocalModeUsage = true; + logger.info( + "You are using Images local mode. This only supports resizing, rotating and transcoding." + ); + } + } + const upstream = typeof config.localUpstream === "string" ? `${config.upstreamProtocol}://${config.localUpstream}` diff --git a/packages/wrangler/src/images/fetcher.ts b/packages/wrangler/src/images/fetcher.ts new file mode 100644 index 000000000000..49a5f8f25bff --- /dev/null +++ b/packages/wrangler/src/images/fetcher.ts @@ -0,0 +1,35 @@ +import { Response } from "miniflare"; +import { performApiFetch } from "../cfetch/internal"; +import { getAccountId } from "../user"; +import type { Request } from "miniflare"; + +export const EXTERNAL_IMAGES_WORKER_NAME = "__WRANGLER_EXTERNAL_IMAGES_WORKER"; + +export const EXTERNAL_IMAGES_WORKER_SCRIPT = ` +import makeBinding from 'cloudflare-internal:images-api' + +export default function (env) { + return makeBinding({ + fetcher: env.FETCHER, + }); +} +`; + +export async function imagesRemoteFetcher(request: Request): Promise { + const accountId = await getAccountId(); + + const url = `/accounts/${accountId}/images_edge/v2/binding/preview${new URL(request.url).pathname}`; + + const res = await performApiFetch(url, { + method: request.method, + body: request.body, + duplex: "half", + headers: { + "content-type": request.headers.get("content-type") || "", + }, + }); + + return new Response(res.body, { headers: res.headers }); +} + +export { imagesLocalFetcher } from "./local"; diff --git a/packages/wrangler/src/images/local.ts b/packages/wrangler/src/images/local.ts new file mode 100644 index 000000000000..d127fce0dec0 --- /dev/null +++ b/packages/wrangler/src/images/local.ts @@ -0,0 +1,221 @@ +import { File } from "buffer"; +import type { ImageInfoResponse } from "@cloudflare/workers-types/experimental"; +import type { Sharp } from "sharp"; + +type Transform = { + imageIndex?: number; + rotate?: number; + width?: number; + height?: number; +}; + +function validateTransforms(inputTransforms: unknown): Transform[] | null { + if (!Array.isArray(inputTransforms)) { + return null; + } + + for (const transform of inputTransforms) { + for (const key of ["imageIndex", "rotate", "width", "height"]) { + if (transform[key] !== undefined && typeof transform[key] != "number") { + return null; + } + } + } + + return inputTransforms as Transform[]; +} + +export async function imagesLocalFetcher(request: Request): Promise { + let sharp; + try { + const { default: importedSharp } = await import("sharp"); + sharp = importedSharp; + } catch { + // This should be unreachable, as we should have errored by now + // if sharp isn't installed + return errorResponse( + 503, + 9523, + "The Sharp library is not available, check your version of Node is compatible" + ); + } + + const data = await request.formData(); + + const body = data.get("image"); + if (!body || !(body instanceof File)) { + return errorResponse( + 400, + 9523, + `ERROR: Internal Images binding error: expected image in request, got ${body}` + ); + } + + const transformer = sharp(await body.arrayBuffer(), {}); + + const url = new URL(request.url); + + if (url.pathname == "/info") { + return runInfo(transformer); + } else { + const badTransformsResponse = errorResponse( + 400, + 9523, + "ERROR: Internal Images binding error: Expected JSON array of valid transforms in transforms field" + ); + try { + const transformsJson = data.get("transforms"); + + if (typeof transformsJson !== "string") { + return badTransformsResponse; + } + + const transforms = validateTransforms(JSON.parse(transformsJson)); + + if (transforms === null) { + return badTransformsResponse; + } + + const outputFormat = data.get("output_format"); + + if (outputFormat != null && typeof outputFormat !== "string") { + return errorResponse( + 400, + 9523, + "ERROR: Internal Images binding error: Expected output format to be a string if provided" + ); + } + + return runTransform(transformer, transforms, outputFormat); + } catch (e) { + return badTransformsResponse; + } + } +} + +async function runInfo(transformer: Sharp): Promise { + const metadata = await transformer.metadata(); + + let mime: string | null = null; + switch (metadata.format) { + case "jpeg": + mime = "image/jpeg"; + break; + case "svg": + mime = "image/svg+xml"; + break; + case "png": + mime = "image/png"; + break; + case "webp": + mime = "image/webp"; + break; + case "gif": + mime = "image/gif"; + break; + case "avif": + mime = "image/avif"; + break; + default: + return errorResponse( + 415, + 9520, + `ERROR: Unsupported image type ${metadata.format}, expected one of: JPEG, SVG, PNG, WebP, GIF or AVIF` + ); + } + + let resp: ImageInfoResponse; + if (mime == "image/svg+xml") { + resp = { + format: mime, + }; + } else { + if (!metadata.size || !metadata.width || !metadata.height) { + return errorResponse( + 500, + 9523, + "ERROR: Internal Images binding error: Expected size, width and height for bitmap input" + ); + } + + resp = { + format: mime, + fileSize: metadata.size, + width: metadata.width, + height: metadata.height, + }; + } + + return Response.json(resp); +} + +async function runTransform( + transformer: Sharp, + transforms: Transform[], + outputFormat: string | null +): Promise { + for (const transform of transforms) { + if (transform.imageIndex !== undefined && transform.imageIndex !== 0) { + // We don't support draws, and this transform doesn't apply to the root + // image, so skip it + continue; + } + + if (transform.rotate !== undefined) { + transformer.rotate(transform.rotate); + } + + if (transform.width !== undefined || transform.height !== undefined) { + transformer.resize(transform.width || null, transform.height || null, { + fit: "contain", + }); + } + } + + switch (outputFormat) { + case "image/avif": + transformer.avif(); + break; + case "image/gif": + return errorResponse( + 415, + 9520, + "ERROR: GIF output is not supported in local mode" + ); + case "image/jpeg": + transformer.jpeg(); + break; + case "image/png": + transformer.png(); + break; + case "image/webp": + transformer.webp(); + break; + case "rgb": + case "rgba": + return errorResponse( + 415, + 9520, + "ERROR: RGB/RGBA output is not supported in local mode" + ); + default: + outputFormat = "image/jpeg"; + break; + } + + return new Response(transformer, { + headers: { + "content-type": outputFormat, + }, + }); +} + +function errorResponse(status: number, code: number, message: string) { + return new Response(`ERROR ${code}: ${message}`, { + status, + headers: { + "content-type": "text/plain", + "cf-images-binding": `err=${code}`, + }, + }); +} diff --git a/packages/wrangler/src/init.ts b/packages/wrangler/src/init.ts index 91dc5d968dac..40007ffcfd91 100644 --- a/packages/wrangler/src/init.ts +++ b/packages/wrangler/src/init.ts @@ -1073,6 +1073,13 @@ export async function mapBindings( }; } break; + case "images": + { + configObj.images = { + binding: binding.name, + }; + } + break; case "r2_bucket": { configObj.r2_buckets = [ diff --git a/packages/wrangler/src/pages/dev.ts b/packages/wrangler/src/pages/dev.ts index 0b224531f30a..8332ee8c85cf 100644 --- a/packages/wrangler/src/pages/dev.ts +++ b/packages/wrangler/src/pages/dev.ts @@ -240,6 +240,12 @@ export function Options(yargs: CommonYargsArgv) { "Bind to production Vectorize indexes in local development mode", default: false, }, + "experimental-images-local-mode": { + type: "boolean", + describe: + "Use a local lower-fidelity implementation of the Images binding", + default: false, + }, }); } @@ -944,6 +950,7 @@ export const Handler = async (args: PagesDevArguments) => { logLevel: args.logLevel ?? "log", experimentalProvision: undefined, experimentalVectorizeBindToProd: false, + experimentalImagesLocalMode: false, enableIpc: true, config: Array.isArray(args.config) ? args.config : undefined, legacyAssets: undefined, diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index de08ddd1c055..b50d12169ed7 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -97,6 +97,7 @@ async function createDraftWorker({ wasm_modules: {}, browser: undefined, ai: undefined, + images: undefined, version_metadata: undefined, text_blobs: {}, data_blobs: {}, diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 16506f7eab55..aad0cd0ae0b0 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -34,6 +34,7 @@ export const friendlyBindingNames: Record< text_blobs: "Text Blobs", browser: "Browser", ai: "AI", + images: "Images", version_metadata: "Worker Version Metadata", unsafe: "Unsafe Metadata", vars: "Vars", @@ -53,6 +54,7 @@ export function printBindings( context: { registry?: WorkerRegistry | null; local?: boolean; + imagesLocalMode?: boolean; name?: string; provisioning?: boolean; } = {} @@ -90,6 +92,7 @@ export function printBindings( text_blobs, browser, ai, + images, version_metadata, unsafe, vars, @@ -344,6 +347,18 @@ export function printBindings( }); } + if (images !== undefined) { + output.push({ + name: friendlyBindingNames.images, + entries: [ + { + key: "Name", + value: addLocalSuffix(images.binding, !!context.imagesLocalMode), + }, + ], + }); + } + if (ai !== undefined) { const entries: [{ key: string; value: string | boolean }] = [ { key: "Name", value: ai.binding }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e91c87393805..6600d35596e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1294,7 +1294,7 @@ importers: version: 8.57.0 eslint-config-turbo: specifier: latest - version: 2.3.4(eslint@8.57.0)(turbo@2.2.3) + version: 2.4.0(eslint@8.57.0)(turbo@2.2.3) eslint-plugin-import: specifier: 2.26.x version: 2.26.0(@typescript-eslint/parser@6.10.0(eslint@8.57.0)(typescript@5.7.3))(eslint@8.57.0) @@ -2553,6 +2553,9 @@ importers: fsevents: specifier: ~2.3.2 version: 2.3.3 + sharp: + specifier: ^0.33.5 + version: 0.33.5 devDependencies: '@aws-sdk/client-s3': specifier: ^3.721.0 @@ -3525,6 +3528,9 @@ packages: resolution: {integrity: sha512-LzxlLEMbBOPYB85uXrDqvD4MgcenjRBLIns3zyhx7vTPj/0u2eQhzXvPiGcaJrV38Q9dbkExWp6cOHPJ+EtFYg==} engines: {node: '>= 6'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@esbuild-kit/cjs-loader@2.4.4': resolution: {integrity: sha512-NfsJX4PdzhwSkfJukczyUiZGc7zNNWZcEAyqeISpDnn0PTfzMJR1aR8xAIPskBejIxBJbIgCCMzbaYa9SXepIg==} deprecated: 'Merged into tsx: https://tsx.is' @@ -4453,6 +4459,111 @@ packages: '@iarna/toml@3.0.0': resolution: {integrity: sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/confirm@3.2.0': resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} engines: {node: '>=18'} @@ -6640,10 +6751,17 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} hasBin: true + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -7086,6 +7204,10 @@ packages: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -7365,8 +7487,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-config-turbo@2.3.4: - resolution: {integrity: sha512-MxPl+IKkR7mRGcHoiZAMHYl+RZnjqBsxTLf+IGnx8BrJQe9/CoLT7oBlUxXGvh9bsd5MTaqCxly5h8BE1v/7AA==} + eslint-config-turbo@2.4.0: + resolution: {integrity: sha512-AiRdy83iwyG4+iMSxXQGUbEClxkGxSlXYH8E2a+0972ao75OWnlDBiiuLMOzDpJubR+QVGC4zonn29AIFCSbFw==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -7441,8 +7563,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-turbo@2.3.4: - resolution: {integrity: sha512-9ncoUJkQGkC28NmlQiS17oB9mrE8XaSulRZiB5pv9vmRbYjOfUwyGhY3EIcoBRdww81igxOzXmAmvNNd6GFBPg==} + eslint-plugin-turbo@2.4.0: + resolution: {integrity: sha512-qCgoRi/OTc1VMxab7+sdKiV1xlkY4qjK9sM+kS7+WogrB1DxLguJSQXvk4HA13SD5VmJsq+8FYOw5q4EUk6Ixg==} peerDependencies: eslint: '>6.6.0' turbo: '>2.0.0' @@ -8193,6 +8315,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -10489,6 +10614,10 @@ packages: shallow-equal@1.2.1: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -10551,6 +10680,9 @@ packages: simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@3.0.0: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} engines: {node: '>=18'} @@ -13140,6 +13272,11 @@ snapshots: tunnel-agent: 0.6.0 uuid: 8.3.2 + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild-kit/cjs-loader@2.4.4': dependencies: '@esbuild-kit/core-utils': 3.3.2 @@ -13592,7 +13729,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.1 @@ -13639,7 +13776,7 @@ snapshots: '@humanwhocodes/config-array@0.11.14': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -13666,6 +13803,81 @@ snapshots: '@iarna/toml@3.0.0': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@inquirer/confirm@3.2.0': dependencies: '@inquirer/core': 9.2.1 @@ -15128,7 +15340,7 @@ snapshots: '@typescript-eslint/type-utils': 6.10.0(eslint@8.57.0)(typescript@5.7.3) '@typescript-eslint/utils': 6.10.0(eslint@8.57.0)(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 graphemer: 1.4.0 ignore: 5.3.1 @@ -15164,7 +15376,7 @@ snapshots: '@typescript-eslint/types': 6.10.0 '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: typescript: 5.7.3 @@ -15177,7 +15389,7 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 optionalDependencies: typescript: 5.6.3 @@ -15198,7 +15410,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.7.3) '@typescript-eslint/utils': 6.10.0(eslint@8.57.0)(typescript@5.7.3) - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.7.3) optionalDependencies: @@ -15210,7 +15422,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) '@typescript-eslint/utils': 7.18.0(eslint@8.57.0)(typescript@5.6.3) - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) eslint: 8.57.0 ts-api-utils: 1.4.3(typescript@5.6.3) optionalDependencies: @@ -15226,7 +15438,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.10.0 '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.6.3 @@ -15240,7 +15452,7 @@ snapshots: dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 @@ -15363,7 +15575,7 @@ snapshots: '@verdaccio/loaders@8.0.0-next-8.4': dependencies: - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) lodash: 4.17.21 transitivePeerDependencies: - supports-color @@ -15424,7 +15636,7 @@ snapshots: '@verdaccio/signature@8.0.0-next-8.1': dependencies: - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) jsonwebtoken: 9.0.2 transitivePeerDependencies: - supports-color @@ -15971,7 +16183,7 @@ snapshots: common-path-prefix: 3.0.0 concordance: 5.0.4 currently-unhandled: 0.4.1 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) emittery: 1.0.1 figures: 6.0.1 globby: 14.0.1 @@ -16439,8 +16651,20 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + color-support@1.1.3: {} + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + colord@2.9.3: {} colorette@2.0.20: {} @@ -16878,6 +17102,9 @@ snapshots: detect-libc@2.0.2: {} + detect-libc@2.0.3: + optional: true + detect-newline@3.1.0: {} devalue@4.3.2: {} @@ -17137,7 +17364,7 @@ snapshots: esbuild-register@3.5.0(esbuild@0.17.19): dependencies: - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.17.19 transitivePeerDependencies: - supports-color @@ -17323,10 +17550,10 @@ snapshots: dependencies: eslint: 8.57.0 - eslint-config-turbo@2.3.4(eslint@8.57.0)(turbo@2.2.3): + eslint-config-turbo@2.4.0(eslint@8.57.0)(turbo@2.2.3): dependencies: eslint: 8.57.0 - eslint-plugin-turbo: 2.3.4(eslint@8.57.0)(turbo@2.2.3) + eslint-plugin-turbo: 2.4.0(eslint@8.57.0)(turbo@2.2.3) turbo: 2.2.3 eslint-import-resolver-node@0.3.7: @@ -17411,7 +17638,7 @@ snapshots: semver: 6.3.1 string.prototype.matchall: 4.0.8 - eslint-plugin-turbo@2.3.4(eslint@8.57.0)(turbo@2.2.3): + eslint-plugin-turbo@2.4.0(eslint@8.57.0)(turbo@2.2.3): dependencies: dotenv: 16.0.3 eslint: 8.57.0 @@ -17452,7 +17679,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -18177,7 +18404,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.3 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18203,6 +18430,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.3 + debug: 4.3.7(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.2(supports-color@9.2.2): dependencies: agent-base: 7.1.3 @@ -18213,7 +18447,7 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.3 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18335,6 +18569,9 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: + optional: true + is-async-function@2.0.0: dependencies: has-tostringtag: 1.0.2 @@ -19506,7 +19743,7 @@ snapshots: debug: 4.4.0(supports-color@9.2.2) get-uri: 6.0.1 http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.2(supports-color@9.2.2) + https-proxy-agent: 7.0.2 pac-resolver: 7.0.0 socks-proxy-agent: 8.0.2 transitivePeerDependencies: @@ -20091,7 +20328,7 @@ snapshots: agent-base: 7.1.3 debug: 4.4.0(supports-color@9.2.2) http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.2(supports-color@9.2.2) + https-proxy-agent: 7.0.2 lru-cache: 7.18.3 pac-proxy-agent: 7.0.1 proxy-from-env: 1.1.0 @@ -20662,6 +20899,33 @@ snapshots: shallow-equal@1.2.1: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -20731,6 +20995,11 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + sirv@3.0.0: dependencies: '@polka/url': 1.0.0-next.25 @@ -21303,7 +21572,7 @@ snapshots: cac: 6.7.14 chokidar: 3.6.0 consola: 3.3.3 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) esbuild: 0.23.1 execa: 5.1.1 joycon: 3.1.1 @@ -21770,7 +22039,7 @@ snapshots: '@volar/typescript': 2.3.4 '@vue/language-core': 2.0.29(typescript@5.7.3) compare-versions: 6.1.1 - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) kolorist: 1.8.0 local-pkg: 0.5.0 magic-string: 0.30.17 @@ -21785,7 +22054,7 @@ snapshots: vite-tsconfig-paths@4.2.0(typescript@5.7.3)(vite@5.0.12(@types/node@18.19.74)): dependencies: - debug: 4.3.7(supports-color@9.2.2) + debug: 4.3.7(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 2.1.1(typescript@5.7.3) optionalDependencies: