diff --git a/.changeset/witty-ideas-rhyme.md b/.changeset/witty-ideas-rhyme.md new file mode 100644 index 000000000000..e885427b5245 --- /dev/null +++ b/.changeset/witty-ideas-rhyme.md @@ -0,0 +1,5 @@ +--- +"wrangler": minor +--- + +Stabilise Worker Loader bindings diff --git a/fixtures/dynamic-worker-loading/wrangler.jsonc b/fixtures/dynamic-worker-loading/wrangler.jsonc index c62ac4d80bf6..76ebfc6cd3de 100644 --- a/fixtures/dynamic-worker-loading/wrangler.jsonc +++ b/fixtures/dynamic-worker-loading/wrangler.jsonc @@ -2,12 +2,9 @@ "name": "dynamic-worker-loading", "main": "src/index.ts", "compatibility_date": "2023-05-04", - "unsafe": { - "bindings": [ - { - "name": "LOADER", - "type": "worker-loader", - }, - ], - }, + "worker_loaders": [ + { + "binding": "LOADER", + }, + ], } diff --git a/packages/miniflare/src/plugins/worker-loader/index.ts b/packages/miniflare/src/plugins/worker-loader/index.ts index e4c09ea1800e..8dcc11bc2a67 100644 --- a/packages/miniflare/src/plugins/worker-loader/index.ts +++ b/packages/miniflare/src/plugins/worker-loader/index.ts @@ -2,9 +2,7 @@ import { z } from "zod"; import { Worker_Binding } from "../../runtime"; import { Plugin } from "../shared"; -export const WorkerLoaderConfigSchema = z.object({ - id: z.string().optional(), -}); +export const WorkerLoaderConfigSchema = z.object({}); export const WorkerLoaderOptionsSchema = z.object({ workerLoaders: z.record(WorkerLoaderConfigSchema).optional(), }); diff --git a/packages/wrangler/src/__tests__/config/configuration.test.ts b/packages/wrangler/src/__tests__/config/configuration.test.ts index 8ed5d1d37cdf..cc7eeaaea0bf 100644 --- a/packages/wrangler/src/__tests__/config/configuration.test.ts +++ b/packages/wrangler/src/__tests__/config/configuration.test.ts @@ -7,6 +7,7 @@ import { normalizeString } from "../helpers/normalize"; import { runInTempDir } from "../helpers/run-in-tmp"; import { writeWranglerConfig } from "../helpers/write-wrangler-config"; import type { + Config, ConfigFields, RawConfig, RawDevConfig, @@ -78,6 +79,8 @@ describe("normalizeAndValidateConfig()", () => { upstream_protocol: "http", host: undefined, enable_containers: true, + inspector_port: undefined, + container_engine: undefined, }, containers: undefined, cloudchamber: {}, @@ -92,7 +95,6 @@ describe("normalizeAndValidateConfig()", () => { legacy_env: true, logfwdr: { bindings: [], - schema: undefined, }, send_metrics: undefined, main: undefined, @@ -125,7 +127,6 @@ describe("normalizeAndValidateConfig()", () => { }, dispatch_namespaces: [], mtls_certificates: [], - usage_model: undefined, vars: {}, define: {}, definedEnvironments: [], @@ -134,7 +135,6 @@ describe("normalizeAndValidateConfig()", () => { data_blobs: undefined, workers_dev: undefined, preview_urls: undefined, - zone_id: undefined, no_bundle: undefined, minify: undefined, first_party_worker: undefined, @@ -142,10 +142,23 @@ describe("normalizeAndValidateConfig()", () => { logpush: undefined, upload_source_maps: undefined, placement: undefined, + worker_loaders: [], tail_consumers: undefined, pipelines: [], workflows: [], - }); + userConfigPath: undefined, + topLevelName: undefined, + alias: undefined, + find_additional_modules: undefined, + preserve_file_names: undefined, + base_dir: undefined, + limits: undefined, + keep_names: undefined, + assets: undefined, + observability: undefined, + compliance_region: undefined, + images: undefined, + } satisfies Config); expect(diagnostics.hasErrors()).toBe(false); expect(diagnostics.hasWarnings()).toBe(false); }); @@ -3978,6 +3991,91 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[worker_loaders]", () => { + it("should error if worker_loaders is an object", () => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { worker_loaders: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"worker_loaders\\" should be an array but got {}." + `); + }); + + it("should error if worker_loaders is null", () => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { worker_loaders: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field \\"worker_loaders\\" should be an array but got null." + `); + }); + + it("should accept valid bindings", () => { + const { diagnostics } = normalizeAndValidateConfig( + { + worker_loaders: [ + { + binding: "VALID", + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + }); + + it("should error if worker_loaders bindings are not valid", () => { + const { diagnostics } = normalizeAndValidateConfig( + { + worker_loaders: [ + // @ts-expect-error Test if empty object is caught + {}, + { + binding: "VALID", + }, + { + // @ts-expect-error Test if binding is not a string + binding: null, + invalid: true, + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in worker_loaders[2] field: \\"invalid\\"" + `); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - \\"worker_loaders[0]\\" bindings must have a string \\"binding\\" field but got {}. + - \\"worker_loaders[2]\\" bindings must have a string \\"binding\\" field but got {\\"binding\\":null,\\"invalid\\":true}." + `); + }); + }); + describe("[unsafe_hello_world]", () => { it("should error if unsafe_hello_world is an object", () => { const { diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 1466d8f6af56..6cca2cb249eb 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -263,6 +263,11 @@ const bindingsConfigMock: Omit< }, }, ], + worker_loaders: [ + { + binding: "WORKER_LOADER_BINDING", + }, + ], }; describe("generate types", () => { @@ -480,6 +485,7 @@ describe("generate types", () => { SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; RATE_LIMITER: RateLimit; + WORKER_LOADER_BINDING: WorkerLoader; SERVICE_BINDING: Fetcher /* service_name */; OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */; OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */; @@ -579,6 +585,7 @@ describe("generate types", () => { SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; RATE_LIMITER: RateLimit; + WORKER_LOADER_BINDING: WorkerLoader; SERVICE_BINDING: Fetcher /* service_name */; OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */; OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */; @@ -742,6 +749,7 @@ describe("generate types", () => { SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; RATE_LIMITER: RateLimit; + WORKER_LOADER_BINDING: WorkerLoader; SERVICE_BINDING: Service; OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */; OTHER_SERVICE_BINDING_ENTRYPOINT: Service; diff --git a/packages/wrangler/src/api/startDevWorker/types.ts b/packages/wrangler/src/api/startDevWorker/types.ts index cc2b074a8952..2b5d135b6965 100644 --- a/packages/wrangler/src/api/startDevWorker/types.ts +++ b/packages/wrangler/src/api/startDevWorker/types.ts @@ -32,6 +32,7 @@ import type { CfTailConsumer, CfUnsafe, CfVectorize, + CfWorkerLoader, CfWorkflow, } from "../../deployment-bundle/worker"; import type { CfAccount } from "../../dev/create-worker-preview"; @@ -304,6 +305,7 @@ export type Binding = | ({ type: "logfwdr" } & NameOmit) | ({ type: "unsafe_hello_world" } & BindingOmit) | ({ type: "ratelimit" } & NameOmit) + | ({ type: "worker_loader" } & BindingOmit) | { type: `unsafe_${string}` } | { type: "assets" }; diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index c0b7a15caeb5..6411471d0440 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -287,6 +287,12 @@ export function convertCfWorkerInitBindingsToBindings( } break; } + case "worker_loaders": { + for (const { binding, ...x } of info) { + output[binding] = { type: "worker_loader", ...x }; + } + break; + } default: { assertNever(type); } @@ -331,6 +337,7 @@ export async function convertBindingsToCfWorkerInitBindings( pipelines: undefined, unsafe_hello_world: undefined, ratelimits: undefined, + worker_loaders: undefined, }; const fetchers: Record = {}; @@ -423,6 +430,9 @@ export async function convertBindingsToCfWorkerInitBindings( } else if (binding.type === "ratelimit") { bindings.ratelimits ??= []; bindings.ratelimits.push({ ...binding, name: name }); + } else if (binding.type === "worker_loader") { + bindings.worker_loaders ??= []; + bindings.worker_loaders.push({ ...binding, binding: name }); } else if (isUnsafeBindingType(binding.type)) { bindings.unsafe ??= { bindings: [], diff --git a/packages/wrangler/src/config/config.ts b/packages/wrangler/src/config/config.ts index a40bedb44606..c89c083d462d 100644 --- a/packages/wrangler/src/config/config.ts +++ b/packages/wrangler/src/config/config.ts @@ -335,6 +335,7 @@ export const defaultWranglerConfig: Config = { version_metadata: undefined, unsafe_hello_world: [], ratelimits: [], + worker_loaders: [], /*====================================================*/ /* Fields supported by Workers only */ diff --git a/packages/wrangler/src/config/environment.ts b/packages/wrangler/src/config/environment.ts index 00cd861a7c7e..ff31767820ce 100644 --- a/packages/wrangler/src/config/environment.ts +++ b/packages/wrangler/src/config/environment.ts @@ -1173,6 +1173,20 @@ export interface EnvironmentNonInheritable { period: 10 | 60; }; }[]; + + /** + * Specifies Worker Loader bindings that are bound to this Worker environment. + * + * NOTE: This field is not automatically inherited from the top level environment, + * and so must be specified in every named environment. + * + * @default [] + * @nonInheritable + */ + worker_loaders: { + /** The binding name used to refer to the Worker Loader in the Worker. */ + binding: string; + }[]; } /** diff --git a/packages/wrangler/src/config/validation.ts b/packages/wrangler/src/config/validation.ts index fe124f542a9e..33b3805e9b8e 100644 --- a/packages/wrangler/src/config/validation.ts +++ b/packages/wrangler/src/config/validation.ts @@ -1454,6 +1454,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateHelloWorldBinding), [] ), + worker_loaders: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "worker_loaders", + validateBindingArray(envName, validateWorkerLoaderBinding), + [] + ), ratelimits: notInheritable( diagnostics, topLevelEnv, @@ -2385,6 +2395,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "logfwdr", "mtls_certificate", "pipeline", + "worker-loader", ]; if (safeBindings.includes(value.type)) { @@ -3867,6 +3878,34 @@ const validateHelloWorldBinding: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateWorkerLoaderBinding: ValidatorFn = ( + diagnostics, + field, + value +) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"worker_loader" 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; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + ]); + + return isValid; +}; + const validateRateLimitBinding: ValidatorFn = (diagnostics, field, value) => { if (typeof value !== "object" || value === null) { diagnostics.errors.push( diff --git a/packages/wrangler/src/deployment-bundle/bindings.ts b/packages/wrangler/src/deployment-bundle/bindings.ts index 603285c6bbfb..7b2e1cd0b377 100644 --- a/packages/wrangler/src/deployment-bundle/bindings.ts +++ b/packages/wrangler/src/deployment-bundle/bindings.ts @@ -76,6 +76,7 @@ export function getBindings( }, unsafe_hello_world: options?.pages ? undefined : config?.unsafe_hello_world, ratelimits: config?.ratelimits, + worker_loaders: config?.worker_loaders, }; } 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 109d9d73b05a..1403988363cf 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -158,6 +158,10 @@ export type WorkerMetadataBinding = namespace_id: string; simple: { limit: number; period: 10 | 60 }; } + | { + type: "worker-loader"; + name: string; + } | { type: "logfwdr"; name: string; @@ -499,6 +503,13 @@ export function createWorkerUploadForm(worker: CfWorkerInit): FormData { }); }); + bindings.worker_loaders?.forEach(({ binding }) => { + metadataBindings.push({ + name: binding, + type: "worker-loader", + }); + }); + bindings.logfwdr?.bindings.forEach(({ name, destination }) => { metadataBindings.push({ name: name, diff --git a/packages/wrangler/src/deployment-bundle/worker.ts b/packages/wrangler/src/deployment-bundle/worker.ts index 6f87611a6af8..3b4bd2e014a3 100644 --- a/packages/wrangler/src/deployment-bundle/worker.ts +++ b/packages/wrangler/src/deployment-bundle/worker.ts @@ -226,6 +226,10 @@ export interface CfHelloWorld { enable_timer?: boolean; } +export interface CfWorkerLoader { + binding: string; +} + export interface CfRateLimit { name: string; namespace_id: string; @@ -416,6 +420,7 @@ export interface CfWorkerInit { assets: CfAssetsBinding | undefined; unsafe_hello_world: CfHelloWorld[] | undefined; ratelimits: CfRateLimit[] | undefined; + worker_loaders: CfWorkerLoader[] | undefined; }; containers?: { class_name: string }[]; diff --git a/packages/wrangler/src/dev.ts b/packages/wrangler/src/dev.ts index 582bf5606f6d..107ef87d9f02 100644 --- a/packages/wrangler/src/dev.ts +++ b/packages/wrangler/src/dev.ts @@ -1015,6 +1015,7 @@ export function getBindings( : undefined, unsafe_hello_world: configParam.unsafe_hello_world, ratelimits: configParam.ratelimits, + worker_loaders: configParam.worker_loaders, }; return bindings; diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index de203fe23dee..fc6b46f5baa2 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -743,9 +743,7 @@ export function buildMiniflareBindingOptions( ]) ?? [] ), workerLoaders: Object.fromEntries( - bindings.unsafe?.bindings - ?.filter((b) => b.type == "worker-loader") - .map((binding) => [binding.name, {}]) ?? [] + bindings.worker_loaders?.map(({ binding }) => [binding, {}]) ?? [] ), email: { send_email: bindings.send_email?.map((b) => ({ diff --git a/packages/wrangler/src/secret/index.ts b/packages/wrangler/src/secret/index.ts index bbc0b5c62672..76ccce31872b 100644 --- a/packages/wrangler/src/secret/index.ts +++ b/packages/wrangler/src/secret/index.ts @@ -111,6 +111,7 @@ async function createDraftWorker({ capnp: undefined, }, unsafe_hello_world: [], + worker_loaders: [], }, modules: [], migrations: undefined, diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 3db4901de1f5..8b22b2f0bf92 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -349,6 +349,7 @@ export async function generateEnvTypes( secrets_store_secrets: config.secrets_store_secrets, unsafe_hello_world: config.unsafe_hello_world, ratelimits: config.ratelimits, + worker_loaders: config.worker_loaders, }; const entrypointFormat = entrypoint?.format ?? "modules"; @@ -462,6 +463,15 @@ export async function generateEnvTypes( } } + if (configToDTS.worker_loaders) { + for (const workerLoader of configToDTS.worker_loaders) { + envTypeStructure.push([ + constructTypeKey(workerLoader.binding), + "WorkerLoader", + ]); + } + } + if (configToDTS.services) { for (const service of configToDTS.services) { const serviceEntry = diff --git a/packages/wrangler/src/utils/map-worker-metadata-bindings.ts b/packages/wrangler/src/utils/map-worker-metadata-bindings.ts index 39cbe130448a..9ab3fa6158f5 100644 --- a/packages/wrangler/src/utils/map-worker-metadata-bindings.ts +++ b/packages/wrangler/src/utils/map-worker-metadata-bindings.ts @@ -319,6 +319,16 @@ export async function mapWorkerMetadataBindings( ]; } break; + case "worker-loader": + { + configObj.worker_loaders = [ + ...(configObj.worker_loaders ?? []), + { + binding: binding.name, + }, + ]; + } + break; case "ratelimit": { configObj.ratelimits = [ diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 272683aee904..51f0649dfd6c 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -39,6 +39,7 @@ export const friendlyBindingNames: Record< ratelimits: "Rate Limit", assets: "Assets", unsafe_hello_world: "Hello World", + worker_loaders: "Worker Loader", } as const; /**