diff --git a/.changeset/artifacts-binding-support.md b/.changeset/artifacts-binding-support.md new file mode 100644 index 0000000000..b4aeef0525 --- /dev/null +++ b/.changeset/artifacts-binding-support.md @@ -0,0 +1,24 @@ +--- +"wrangler": minor +"miniflare": minor +"@cloudflare/workers-utils": minor +--- + +Add Artifacts binding support to wrangler + +You can now configure Artifacts bindings in your wrangler configuration: + +```jsonc +// wrangler.jsonc +{ + "artifacts": [{ "binding": "MY_ARTIFACTS", "namespace": "default" }], +} +``` + +Type generation produces the correct `Artifacts` type reference from the workerd type definitions: + +```ts +interface Env { + MY_ARTIFACTS: Artifacts; +} +``` diff --git a/packages/miniflare/src/plugins/artifacts/index.ts b/packages/miniflare/src/plugins/artifacts/index.ts new file mode 100644 index 0000000000..fbe4823fe8 --- /dev/null +++ b/packages/miniflare/src/plugins/artifacts/index.ts @@ -0,0 +1,67 @@ +import { z } from "zod"; +import { + getUserBindingServiceName, + ProxyNodeBinding, + remoteProxyClientWorker, +} from "../shared"; +import type { Plugin, RemoteProxyConnectionString } from "../shared"; + +const ArtifactsSchema = z.object({ + namespace: z.string(), + remoteProxyConnectionString: z + .custom() + .optional(), +}); + +export const ArtifactsOptionsSchema = z.object({ + artifacts: z.record(ArtifactsSchema).optional(), +}); + +export const ARTIFACTS_PLUGIN_NAME = "artifacts"; + +export const ARTIFACTS_PLUGIN: Plugin = { + options: ArtifactsOptionsSchema, + async getBindings(options) { + if (!options.artifacts) { + return []; + } + + return Object.entries(options.artifacts).map(([name, config]) => ({ + name, + service: { + name: getUserBindingServiceName( + ARTIFACTS_PLUGIN_NAME, + name, + config.remoteProxyConnectionString + ), + }, + })); + }, + getNodeBindings(options: z.infer) { + if (!options.artifacts) { + return {}; + } + return Object.fromEntries( + Object.keys(options.artifacts).map((name) => [ + name, + new ProxyNodeBinding(), + ]) + ); + }, + async getServices({ options }) { + if (!options.artifacts) { + return []; + } + + return Object.entries(options.artifacts).map( + ([name, { remoteProxyConnectionString }]) => ({ + name: getUserBindingServiceName( + ARTIFACTS_PLUGIN_NAME, + name, + remoteProxyConnectionString + ), + worker: remoteProxyClientWorker(remoteProxyConnectionString, name), + }) + ); + }, +}; diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 164447dd52..7faa127c4f 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -4,6 +4,7 @@ import { ANALYTICS_ENGINE_PLUGIN, ANALYTICS_ENGINE_PLUGIN_NAME, } from "./analytics-engine"; +import { ARTIFACTS_PLUGIN, ARTIFACTS_PLUGIN_NAME } from "./artifacts"; import { ASSETS_PLUGIN } from "./assets"; import { ASSETS_PLUGIN_NAME } from "./assets/constants"; import { @@ -76,6 +77,7 @@ export const PLUGINS = { [MTLS_PLUGIN_NAME]: MTLS_PLUGIN, [HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN, [FLAGSHIP_PLUGIN_NAME]: FLAGSHIP_PLUGIN, + [ARTIFACTS_PLUGIN_NAME]: ARTIFACTS_PLUGIN, [WORKER_LOADER_PLUGIN_NAME]: WORKER_LOADER_PLUGIN, [MEDIA_PLUGIN_NAME]: MEDIA_PLUGIN, [VERSION_METADATA_PLUGIN_NAME]: VERSION_METADATA_PLUGIN, @@ -144,6 +146,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input; @@ -234,6 +237,7 @@ export * from "./vpc-services"; export * from "./mtls"; export * from "./hello-world"; export * from "./flagship"; +export * from "./artifacts"; export * from "./worker-loader"; export * from "./media"; export * from "./version-metadata"; diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index 84139dd709..8402fd89aa 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -339,6 +339,7 @@ export const defaultWranglerConfig: Config = { hyperdrive: [], workflows: [], secrets_store_secrets: [], + artifacts: [], services: [], analytics_engine_datasets: [], ai: undefined, diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 0b5293abe5..cc7e9a6f61 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1341,6 +1341,27 @@ export interface EnvironmentNonInheritable { secret_name: string; }[]; + /** + * Specifies Artifacts bindings that are bound to this Worker environment. + * Artifacts provides git-compatible file storage on Cloudflare Workers. + * + * NOTE: This field is not automatically inherited from the top level environment, + * and so must be specified in every named environment. + * + * @default [] + * @nonInheritable + */ + artifacts: { + /** The binding name used to refer to the Artifacts instance. */ + binding: string; + + /** The namespace to use. */ + namespace: string; + + /** Whether to use the remote Artifacts service in local dev. */ + remote?: boolean; + }[]; + /** * **DO NOT USE**. Hello World Binding Config to serve as an explanatory example. * diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index f37d6c3d2d..7282669c41 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -102,6 +102,7 @@ export type ConfigBindingFieldName = | "workflows" | "pipelines" | "secrets_store_secrets" + | "artifacts" | "ratelimits" | "assets" | "unsafe_hello_world" @@ -143,6 +144,7 @@ export const friendlyBindingNames: Record = { workflows: "Workflow", pipelines: "Pipeline", secrets_store_secrets: "Secrets Store Secret", + artifacts: "Artifacts", ratelimits: "Rate Limit", assets: "Assets", unsafe_hello_world: "Hello World", @@ -187,6 +189,7 @@ const bindingTypeFriendlyNames: Record = { mtls_certificate: "mTLS Certificate", pipeline: "Pipeline", secrets_store_secret: "Secrets Store Secret", + artifacts: "Artifacts", logfwdr: "logfwdr", unsafe_hello_world: "Hello World", flagship: "Flagship", @@ -1878,6 +1881,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateSecretsStoreSecretBinding), [] ), + artifacts: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "artifacts", + validateBindingArray(envName, validateArtifactsBinding), + [] + ), unsafe_hello_world: notInheritable( diagnostics, topLevelEnv, @@ -2997,6 +3010,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "vpc_network", "stream", "media", + "artifacts", ]; if (safeBindings.includes(value.type)) { @@ -4779,6 +4793,45 @@ const validateSecretsStoreSecretBinding: ValidatorFn = ( return isValid; }; +const validateArtifactsBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"artifacts" bindings should be objects, but got ${JSON.stringify(value)}` + ); + return false; + } + let isValid = true; + if (!isRequiredProperty(value, "binding", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "binding" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + if (!isRequiredProperty(value, "namespace", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "namespace" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "namespace", + "remote", + ]); + + if (!isRemoteValid(value, field, diagnostics)) { + isValid = false; + } + + return isValid; +}; + const validateHelloWorldBinding: ValidatorFn = (diagnostics, field, value) => { if (typeof value !== "object" || value === null) { diagnostics.errors.push( @@ -5035,6 +5088,7 @@ const validatePreviewsConfig = "media", "pipelines", "secrets_store_secrets", + "artifacts", "unsafe_hello_world", "worker_loaders", "ratelimits", @@ -5260,6 +5314,14 @@ const validatePreviewsConfig = undefined ) && isValid; + isValid = + validateBindingArray(envName, validateArtifactsBinding)( + diagnostics, + `${field}.artifacts`, + previews.artifacts, + undefined + ) && isValid; + isValid = validateBindingArray(envName, validateHelloWorldBinding)( diagnostics, diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index 2a3d35f5ce..b8b2776122 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -132,6 +132,17 @@ export function mapWorkerMetadataBindings( ]; } break; + case "artifacts": + { + configObj.artifacts = [ + ...(configObj.artifacts ?? []), + { + binding: binding.name, + namespace: binding.namespace, + }, + ]; + } + break; case "unsafe_hello_world": { configObj.unsafe_hello_world = [ ...(configObj.unsafe_hello_world ?? []), diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index 29fbf67289..aa5882c379 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -29,6 +29,7 @@ import type { CfQueue, CfR2Bucket, CfRateLimit, + CfArtifacts, CfSecretsStoreSecrets, CfSendEmailBindings, CfService, @@ -155,6 +156,11 @@ export type WorkerMetadataBinding = store_id: string; secret_name: string; } + | { + type: "artifacts"; + name: string; + namespace: string; + } | { type: "unsafe_hello_world"; name: string; @@ -336,6 +342,7 @@ export type Binding = | ({ type: "mtls_certificate" } & BindingOmit) | ({ type: "pipeline" } & BindingOmit) | ({ type: "secrets_store_secret" } & BindingOmit) + | ({ type: "artifacts" } & BindingOmit) | ({ type: "logfwdr" } & NameOmit) | ({ type: "unsafe_hello_world" } & BindingOmit) | ({ type: "flagship" } & BindingOmit) diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index b57bb93e74..06925fe971 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -248,6 +248,12 @@ export interface CfSecretsStoreSecrets { secret_name: string; } +export interface CfArtifacts { + binding: string; + namespace: string; + remote?: boolean; +} + export interface CfHelloWorld { binding: string; enable_timer?: boolean; diff --git a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts index 8ef8b2f79b..1a4f399770 100644 --- a/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts +++ b/packages/workers-utils/tests/config/validation/normalize-and-validate-config.test.ts @@ -74,6 +74,7 @@ describe("normalizeAndValidateConfig()", () => { }, r2_buckets: [], secrets_store_secrets: [], + artifacts: [], unsafe_hello_world: [], flagship: [], ratelimits: [], @@ -4527,6 +4528,138 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[artifacts]", () => { + it("should error if artifacts is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { artifacts: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "artifacts" should be an array but got {}." + `); + }); + + it("should error if artifacts is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { artifacts: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "artifacts" should be an array but got null." + `); + }); + + it("should accept valid bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should accept valid bindings with remote set to true", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + remote: true, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should error if remote is not a boolean", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + remote: "yes", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "artifacts[0]" should, optionally, have a boolean "remote" field but got {"binding":"MY_ARTIFACTS","namespace":"default","remote":"yes"}." + `); + }); + + it("should error if artifacts bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + artifacts: [ + {}, + { + binding: "VALID", + namespace: "default", + }, + { + binding: null, + invalid: true, + namespace: 123, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in artifacts[2] field: "invalid"" + `); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "artifacts[0]" bindings must have a string "binding" field but got {}. + - "artifacts[0]" bindings must have a string "namespace" field but got {}. + - "artifacts[2]" bindings must have a string "binding" field but got {"binding":null,"invalid":true,"namespace":123}. + - "artifacts[2]" bindings must have a string "namespace" field but got {"binding":null,"invalid":true,"namespace":123}." + `); + }); + }); + describe("[worker_loaders]", () => { it("should error if worker_loaders is an object", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts index f4bfd89d05..df74e34fd4 100644 --- a/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts +++ b/packages/wrangler/src/__tests__/deploy/get-remote-config-diff.test.ts @@ -405,6 +405,12 @@ describe("getRemoteConfigsDiff", () => { instance_name: "my-instance", }, ], + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + }, + ], }, { name: "my-worker-id", @@ -534,6 +540,13 @@ describe("getRemoteConfigsDiff", () => { remote: true, }, ], + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + remote: true, + }, + ], } as unknown as Config ); expect(diff).toBeNull(); diff --git a/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts b/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts index 77e9e7b4cb..5ebc0e5096 100644 --- a/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts +++ b/packages/wrangler/src/__tests__/dev/remote-bindings.test.ts @@ -455,6 +455,33 @@ describe("dev with remote bindings", { sequential: true, retry: 2 }, () => { }), ], }, + { + name: "artifacts", + config: { + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + }, + ], + }, + expectedProxyWorkerBindings: { + MY_ARTIFACTS: { + namespace: "default", + type: "artifacts", + }, + }, + expectedWorkerOptions: [ + expect.objectContaining({ + artifacts: { + MY_ARTIFACTS: { + namespace: "default", + remoteProxyConnectionString, + }, + }, + }), + ], + }, { name: "email", config: { diff --git a/packages/wrangler/src/__tests__/print-bindings.test.ts b/packages/wrangler/src/__tests__/print-bindings.test.ts index b5e7d9defc..599b170f02 100644 --- a/packages/wrangler/src/__tests__/print-bindings.test.ts +++ b/packages/wrangler/src/__tests__/print-bindings.test.ts @@ -99,3 +99,18 @@ describe("printBindings — AI Search bindings", () => { expect(output).toContain("cloudflare-blog"); }); }); + +describe("printBindings -- Artifacts bindings", () => { + it("shows Artifacts bindings", ({ expect }) => { + const output = callPrintBindings({ + MY_ARTIFACTS: { + type: "artifacts", + namespace: "default", + }, + }); + + expect(output).toContain("MY_ARTIFACTS"); + expect(output).toContain("Artifacts"); + expect(output).toContain("default"); + }); +}); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index a9cf718677..0bbe821d09 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -410,6 +410,12 @@ const bindingsConfigMock: Omit< secret_name: "secret_name", }, ], + artifacts: [ + { + binding: "MY_ARTIFACTS", + namespace: "default", + }, + ], unsafe_hello_world: [ { binding: "HELLO_WORLD", @@ -770,6 +776,7 @@ describe("generate types", () => { MTLS_BINDING: Fetcher; TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; + MY_ARTIFACTS: Artifacts; HELLO_WORLD: HelloWorldBinding; FLAGS: Flagship; RATE_LIMITER: RateLimit; @@ -886,6 +893,7 @@ describe("generate types", () => { MTLS_BINDING: Fetcher; TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; + MY_ARTIFACTS: Artifacts; HELLO_WORLD: HelloWorldBinding; FLAGS: Flagship; RATE_LIMITER: RateLimit; @@ -1065,6 +1073,7 @@ describe("generate types", () => { MTLS_BINDING: Fetcher; TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; + MY_ARTIFACTS: Artifacts; HELLO_WORLD: HelloWorldBinding; FLAGS: Flagship; RATE_LIMITER: RateLimit; diff --git a/packages/wrangler/src/api/remoteBindings/index.ts b/packages/wrangler/src/api/remoteBindings/index.ts index 318c52bb19..fb9e8f0264 100644 --- a/packages/wrangler/src/api/remoteBindings/index.ts +++ b/packages/wrangler/src/api/remoteBindings/index.ts @@ -20,7 +20,11 @@ export function pickRemoteBindings( ): Record { return Object.fromEntries( Object.entries(bindings ?? {}).filter(([, binding]) => { - if (binding.type === "ai" || binding.type === "media") { + if ( + binding.type === "ai" || + binding.type === "media" || + binding.type === "artifacts" + ) { // AI and 'media' bindings are always remote return true; } diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index 1db263b8c5..55c49b62d8 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -353,6 +353,12 @@ export function convertConfigToBindings( } break; } + case "artifacts": { + for (const { binding, ...x } of info) { + output[binding] = { type: "artifacts", ...x }; + } + break; + } case "unsafe_hello_world": { if (pages) { break; diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index ec02d5fa4e..e7e4f32149 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -30,6 +30,7 @@ const reorderableBindings = { mtls_certificates: true, pipelines: true, secrets_store_secrets: true, + artifacts: true, ratelimits: true, analytics_engine_datasets: true, unsafe_hello_world: true, @@ -242,6 +243,12 @@ function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { ); } + if (normalizedConfig.artifacts?.length) { + normalizedConfig.artifacts = normalizedConfig.artifacts.map( + ({ remote: _, ...binding }) => binding + ); + } + const singleBindingFields = [ "browser", "ai", 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 1fa2be7b2d..e866bbfcd4 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -136,6 +136,7 @@ export function createWorkerUploadForm( "secrets_store_secret", bindings ); + const artifacts = extractBindingsOfType("artifacts", bindings); const unsafe_hello_world = extractBindingsOfType( "unsafe_hello_world", bindings @@ -377,6 +378,14 @@ export function createWorkerUploadForm( }); }); + artifacts.forEach(({ binding, namespace }) => { + metadataBindings.push({ + name: binding, + type: "artifacts", + namespace, + }); + }); + unsafe_hello_world.forEach(({ binding, enable_timer }) => { metadataBindings.push({ name: binding, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 0571544d15..60598fd90f 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -425,6 +425,7 @@ type WorkerOptionsBindings = Pick< | "mtlsCertificates" | "helloWorld" | "flagship" + | "artifacts" | "workerLoaders" | "unsafeBindings" | "additionalUnboundDurableObjects" @@ -498,6 +499,7 @@ export function buildMiniflareBindingOptions( bindings ); const flagshipBindings = extractBindingsOfType("flagship", bindings); + const artifactsBindings = extractBindingsOfType("artifacts", bindings); const workerLoaders = extractBindingsOfType("worker_loader", bindings); const sendEmailBindings = extractBindingsOfType("send_email", bindings); // Extract both regular and unsafe ratelimit bindings @@ -624,6 +626,10 @@ export function buildMiniflareBindingOptions( warnOrError("media", media.remote, "always-remote"); } + for (const artifact of artifactsBindings) { + warnOrError("artifacts", artifact.remote, "always-remote"); + } + const unsafeBindings: WorkerOptionsBindings["unsafeBindings"] = []; const unsafeBindingsWithLocalDev = Object.entries(bindings ?? {}).filter( (b) => isUnsafeServiceBindingWithDevCfg(b[1]) @@ -799,6 +805,15 @@ export function buildMiniflareBindingOptions( }, ]) ), + artifacts: Object.fromEntries( + artifactsBindings.map((binding) => [ + binding.binding, + { + namespace: binding.namespace, + remoteProxyConnectionString, + }, + ]) + ), workerLoaders: Object.fromEntries( workerLoaders.map(({ binding }) => [binding, {}]) ), diff --git a/packages/wrangler/src/preview/shared.ts b/packages/wrangler/src/preview/shared.ts index ad3e8732e0..470fddd8e4 100644 --- a/packages/wrangler/src/preview/shared.ts +++ b/packages/wrangler/src/preview/shared.ts @@ -115,6 +115,8 @@ export function getBindingValue(binding: Binding): string { return binding.secret_name ? `${binding.store_id}/${binding.secret_name}` : String(binding.store_id ?? ""); + case "artifacts": + return String(binding.namespace ?? ""); case "ratelimit": return String(binding.namespace_id ?? ""); case "vpc_service": @@ -251,6 +253,13 @@ export function extractConfigBindings(config: Config): EnvBindings { }; } + for (const artifact of previews?.artifacts ?? []) { + env[artifact.binding] = { + type: "artifacts", + namespace: artifact.namespace, + }; + } + for (const ratelimit of previews?.ratelimits ?? []) { env[ratelimit.name] = { type: "ratelimit", diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 162dbcb7a7..f1dd171bef 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1797,6 +1797,21 @@ function collectCoreBindings( ); } + for (const [index, artifact] of (env.artifacts ?? []).entries()) { + if (!artifact.binding) { + throwMissingBindingError({ + binding: artifact, + bindingType: "artifacts", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(artifact.binding, "Artifacts", "artifacts", envName); + } + for (const [index, helloWorld] of ( env.unsafe_hello_world ?? [] ).entries()) { @@ -2776,6 +2791,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, artifact] of (env.artifacts ?? []).entries()) { + if (!artifact.binding) { + throwMissingBindingError({ + binding: artifact, + bindingType: "artifacts", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "artifacts", + name: artifact.binding, + type: "Artifacts", + }); + } + for (const [index, helloWorld] of ( env.unsafe_hello_world ?? [] ).entries()) { diff --git a/packages/wrangler/src/utils/add-created-resource-config.ts b/packages/wrangler/src/utils/add-created-resource-config.ts index b36b241cf7..7df058a26d 100644 --- a/packages/wrangler/src/utils/add-created-resource-config.ts +++ b/packages/wrangler/src/utils/add-created-resource-config.ts @@ -41,6 +41,7 @@ type ValidKeys = Exclude< | "mtls_certificates" | "dispatch_namespaces" | "secrets_store_secrets" + | "artifacts" | "unsafe_hello_world" | "flagship" >; diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index c5fa3e2d5b..9f2d162362 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -95,6 +95,7 @@ export function printBindings( "secrets_store_secret", bindings ); + const artifacts = extractBindingsOfType("artifacts", bindings); const services = extractBindingsOfType("service", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); const vpc_networks = extractBindingsOfType("vpc_network", bindings); @@ -441,6 +442,19 @@ export function printBindings( ); } + if (artifacts.length > 0) { + output.push( + ...artifacts.map(({ binding, namespace }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("artifacts"), + value: namespace, + mode: getMode({ isSimulatedLocally: false }), + }; + }) + ); + } + if (unsafe_hello_world.length > 0) { output.push( ...unsafe_hello_world.map(({ binding, enable_timer }) => {