Skip to content

Commit 4a9ba90

Browse files
mattzcareydario-piotrowiczdevin-ai-integration[bot]
authored
[wrangler] add artifacts binding support (#13326)
Co-authored-by: Dario Piotrowicz <dario@cloudflare.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent b35617b commit 4a9ba90

23 files changed

Lines changed: 500 additions & 1 deletion

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
"wrangler": minor
3+
"miniflare": minor
4+
"@cloudflare/workers-utils": minor
5+
---
6+
7+
Add Artifacts binding support to wrangler
8+
9+
You can now configure Artifacts bindings in your wrangler configuration:
10+
11+
```jsonc
12+
// wrangler.jsonc
13+
{
14+
"artifacts": [{ "binding": "MY_ARTIFACTS", "namespace": "default" }],
15+
}
16+
```
17+
18+
Type generation produces the correct `Artifacts` type reference from the workerd type definitions:
19+
20+
```ts
21+
interface Env {
22+
MY_ARTIFACTS: Artifacts;
23+
}
24+
```
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { z } from "zod";
2+
import {
3+
getUserBindingServiceName,
4+
ProxyNodeBinding,
5+
remoteProxyClientWorker,
6+
} from "../shared";
7+
import type { Plugin, RemoteProxyConnectionString } from "../shared";
8+
9+
const ArtifactsSchema = z.object({
10+
namespace: z.string(),
11+
remoteProxyConnectionString: z
12+
.custom<RemoteProxyConnectionString>()
13+
.optional(),
14+
});
15+
16+
export const ArtifactsOptionsSchema = z.object({
17+
artifacts: z.record(ArtifactsSchema).optional(),
18+
});
19+
20+
export const ARTIFACTS_PLUGIN_NAME = "artifacts";
21+
22+
export const ARTIFACTS_PLUGIN: Plugin<typeof ArtifactsOptionsSchema> = {
23+
options: ArtifactsOptionsSchema,
24+
async getBindings(options) {
25+
if (!options.artifacts) {
26+
return [];
27+
}
28+
29+
return Object.entries(options.artifacts).map(([name, config]) => ({
30+
name,
31+
service: {
32+
name: getUserBindingServiceName(
33+
ARTIFACTS_PLUGIN_NAME,
34+
name,
35+
config.remoteProxyConnectionString
36+
),
37+
},
38+
}));
39+
},
40+
getNodeBindings(options: z.infer<typeof ArtifactsOptionsSchema>) {
41+
if (!options.artifacts) {
42+
return {};
43+
}
44+
return Object.fromEntries(
45+
Object.keys(options.artifacts).map((name) => [
46+
name,
47+
new ProxyNodeBinding(),
48+
])
49+
);
50+
},
51+
async getServices({ options }) {
52+
if (!options.artifacts) {
53+
return [];
54+
}
55+
56+
return Object.entries(options.artifacts).map(
57+
([name, { remoteProxyConnectionString }]) => ({
58+
name: getUserBindingServiceName(
59+
ARTIFACTS_PLUGIN_NAME,
60+
name,
61+
remoteProxyConnectionString
62+
),
63+
worker: remoteProxyClientWorker(remoteProxyConnectionString, name),
64+
})
65+
);
66+
},
67+
};

packages/miniflare/src/plugins/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ANALYTICS_ENGINE_PLUGIN,
55
ANALYTICS_ENGINE_PLUGIN_NAME,
66
} from "./analytics-engine";
7+
import { ARTIFACTS_PLUGIN, ARTIFACTS_PLUGIN_NAME } from "./artifacts";
78
import { ASSETS_PLUGIN } from "./assets";
89
import { ASSETS_PLUGIN_NAME } from "./assets/constants";
910
import {
@@ -76,6 +77,7 @@ export const PLUGINS = {
7677
[MTLS_PLUGIN_NAME]: MTLS_PLUGIN,
7778
[HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN,
7879
[FLAGSHIP_PLUGIN_NAME]: FLAGSHIP_PLUGIN,
80+
[ARTIFACTS_PLUGIN_NAME]: ARTIFACTS_PLUGIN,
7981
[WORKER_LOADER_PLUGIN_NAME]: WORKER_LOADER_PLUGIN,
8082
[MEDIA_PLUGIN_NAME]: MEDIA_PLUGIN,
8183
[VERSION_METADATA_PLUGIN_NAME]: VERSION_METADATA_PLUGIN,
@@ -144,6 +146,7 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
144146
z.input<typeof MTLS_PLUGIN.options> &
145147
z.input<typeof HELLO_WORLD_PLUGIN.options> &
146148
z.input<typeof FLAGSHIP_PLUGIN.options> &
149+
z.input<typeof ARTIFACTS_PLUGIN.options> &
147150
z.input<typeof WORKER_LOADER_PLUGIN.options> &
148151
z.input<typeof MEDIA_PLUGIN.options> &
149152
z.input<typeof VERSION_METADATA_PLUGIN.options>;
@@ -234,6 +237,7 @@ export * from "./vpc-services";
234237
export * from "./mtls";
235238
export * from "./hello-world";
236239
export * from "./flagship";
240+
export * from "./artifacts";
237241
export * from "./worker-loader";
238242
export * from "./media";
239243
export * from "./version-metadata";

packages/workers-utils/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ export const defaultWranglerConfig: Config = {
339339
hyperdrive: [],
340340
workflows: [],
341341
secrets_store_secrets: [],
342+
artifacts: [],
342343
services: [],
343344
analytics_engine_datasets: [],
344345
ai: undefined,

packages/workers-utils/src/config/environment.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1341,6 +1341,27 @@ export interface EnvironmentNonInheritable {
13411341
secret_name: string;
13421342
}[];
13431343

1344+
/**
1345+
* Specifies Artifacts bindings that are bound to this Worker environment.
1346+
* Artifacts provides git-compatible file storage on Cloudflare Workers.
1347+
*
1348+
* NOTE: This field is not automatically inherited from the top level environment,
1349+
* and so must be specified in every named environment.
1350+
*
1351+
* @default []
1352+
* @nonInheritable
1353+
*/
1354+
artifacts: {
1355+
/** The binding name used to refer to the Artifacts instance. */
1356+
binding: string;
1357+
1358+
/** The namespace to use. */
1359+
namespace: string;
1360+
1361+
/** Whether to use the remote Artifacts service in local dev. */
1362+
remote?: boolean;
1363+
}[];
1364+
13441365
/**
13451366
* **DO NOT USE**. Hello World Binding Config to serve as an explanatory example.
13461367
*

packages/workers-utils/src/config/validation.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export type ConfigBindingFieldName =
102102
| "workflows"
103103
| "pipelines"
104104
| "secrets_store_secrets"
105+
| "artifacts"
105106
| "ratelimits"
106107
| "assets"
107108
| "unsafe_hello_world"
@@ -143,6 +144,7 @@ export const friendlyBindingNames: Record<ConfigBindingFieldName, string> = {
143144
workflows: "Workflow",
144145
pipelines: "Pipeline",
145146
secrets_store_secrets: "Secrets Store Secret",
147+
artifacts: "Artifacts",
146148
ratelimits: "Rate Limit",
147149
assets: "Assets",
148150
unsafe_hello_world: "Hello World",
@@ -187,6 +189,7 @@ const bindingTypeFriendlyNames: Record<Binding["type"], string> = {
187189
mtls_certificate: "mTLS Certificate",
188190
pipeline: "Pipeline",
189191
secrets_store_secret: "Secrets Store Secret",
192+
artifacts: "Artifacts",
190193
logfwdr: "logfwdr",
191194
unsafe_hello_world: "Hello World",
192195
flagship: "Flagship",
@@ -1878,6 +1881,16 @@ function normalizeAndValidateEnvironment(
18781881
validateBindingArray(envName, validateSecretsStoreSecretBinding),
18791882
[]
18801883
),
1884+
artifacts: notInheritable(
1885+
diagnostics,
1886+
topLevelEnv,
1887+
rawConfig,
1888+
rawEnv,
1889+
envName,
1890+
"artifacts",
1891+
validateBindingArray(envName, validateArtifactsBinding),
1892+
[]
1893+
),
18811894
unsafe_hello_world: notInheritable(
18821895
diagnostics,
18831896
topLevelEnv,
@@ -2997,6 +3010,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => {
29973010
"vpc_network",
29983011
"stream",
29993012
"media",
3013+
"artifacts",
30003014
];
30013015

30023016
if (safeBindings.includes(value.type)) {
@@ -4779,6 +4793,45 @@ const validateSecretsStoreSecretBinding: ValidatorFn = (
47794793
return isValid;
47804794
};
47814795

4796+
const validateArtifactsBinding: ValidatorFn = (diagnostics, field, value) => {
4797+
if (typeof value !== "object" || value === null) {
4798+
diagnostics.errors.push(
4799+
`"artifacts" bindings should be objects, but got ${JSON.stringify(value)}`
4800+
);
4801+
return false;
4802+
}
4803+
let isValid = true;
4804+
if (!isRequiredProperty(value, "binding", "string")) {
4805+
diagnostics.errors.push(
4806+
`"${field}" bindings must have a string "binding" field but got ${JSON.stringify(
4807+
value
4808+
)}.`
4809+
);
4810+
isValid = false;
4811+
}
4812+
4813+
if (!isRequiredProperty(value, "namespace", "string")) {
4814+
diagnostics.errors.push(
4815+
`"${field}" bindings must have a string "namespace" field but got ${JSON.stringify(
4816+
value
4817+
)}.`
4818+
);
4819+
isValid = false;
4820+
}
4821+
4822+
validateAdditionalProperties(diagnostics, field, Object.keys(value), [
4823+
"binding",
4824+
"namespace",
4825+
"remote",
4826+
]);
4827+
4828+
if (!isRemoteValid(value, field, diagnostics)) {
4829+
isValid = false;
4830+
}
4831+
4832+
return isValid;
4833+
};
4834+
47824835
const validateHelloWorldBinding: ValidatorFn = (diagnostics, field, value) => {
47834836
if (typeof value !== "object" || value === null) {
47844837
diagnostics.errors.push(
@@ -5035,6 +5088,7 @@ const validatePreviewsConfig =
50355088
"media",
50365089
"pipelines",
50375090
"secrets_store_secrets",
5091+
"artifacts",
50385092
"unsafe_hello_world",
50395093
"worker_loaders",
50405094
"ratelimits",
@@ -5260,6 +5314,14 @@ const validatePreviewsConfig =
52605314
undefined
52615315
) && isValid;
52625316

5317+
isValid =
5318+
validateBindingArray(envName, validateArtifactsBinding)(
5319+
diagnostics,
5320+
`${field}.artifacts`,
5321+
previews.artifacts,
5322+
undefined
5323+
) && isValid;
5324+
52635325
isValid =
52645326
validateBindingArray(envName, validateHelloWorldBinding)(
52655327
diagnostics,

packages/workers-utils/src/map-worker-metadata-bindings.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,17 @@ export function mapWorkerMetadataBindings(
132132
];
133133
}
134134
break;
135+
case "artifacts":
136+
{
137+
configObj.artifacts = [
138+
...(configObj.artifacts ?? []),
139+
{
140+
binding: binding.name,
141+
namespace: binding.namespace,
142+
},
143+
];
144+
}
145+
break;
135146
case "unsafe_hello_world": {
136147
configObj.unsafe_hello_world = [
137148
...(configObj.unsafe_hello_world ?? []),

packages/workers-utils/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
CfQueue,
3030
CfR2Bucket,
3131
CfRateLimit,
32+
CfArtifacts,
3233
CfSecretsStoreSecrets,
3334
CfSendEmailBindings,
3435
CfService,
@@ -155,6 +156,11 @@ export type WorkerMetadataBinding =
155156
store_id: string;
156157
secret_name: string;
157158
}
159+
| {
160+
type: "artifacts";
161+
name: string;
162+
namespace: string;
163+
}
158164
| {
159165
type: "unsafe_hello_world";
160166
name: string;
@@ -336,6 +342,7 @@ export type Binding =
336342
| ({ type: "mtls_certificate" } & BindingOmit<CfMTlsCertificate>)
337343
| ({ type: "pipeline" } & BindingOmit<CfPipeline>)
338344
| ({ type: "secrets_store_secret" } & BindingOmit<CfSecretsStoreSecrets>)
345+
| ({ type: "artifacts" } & BindingOmit<CfArtifacts>)
339346
| ({ type: "logfwdr" } & NameOmit<CfLogfwdrBinding>)
340347
| ({ type: "unsafe_hello_world" } & BindingOmit<CfHelloWorld>)
341348
| ({ type: "flagship" } & BindingOmit<CfFlagship>)

packages/workers-utils/src/worker.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,12 @@ export interface CfSecretsStoreSecrets {
248248
secret_name: string;
249249
}
250250

251+
export interface CfArtifacts {
252+
binding: string;
253+
namespace: string;
254+
remote?: boolean;
255+
}
256+
251257
export interface CfHelloWorld {
252258
binding: string;
253259
enable_timer?: boolean;

0 commit comments

Comments
 (0)