diff --git a/.changeset/add-flagship-binding.md b/.changeset/add-flagship-binding.md new file mode 100644 index 0000000000..c3db00f7d1 --- /dev/null +++ b/.changeset/add-flagship-binding.md @@ -0,0 +1,9 @@ +--- +"miniflare": minor +"wrangler": minor +"@cloudflare/workers-utils": minor +--- + +feat: add Flagship feature flag binding support + +Adds end-to-end support for the Flagship feature flag binding, which allows Workers to evaluate feature flags from Cloudflare's Flagship service. Configure it in `wrangler.json` with a `flagship` array containing `binding` and `app_id` entries. In local dev, the binding returns default values for all flag evaluations; use `"remote": true` in the binding to evaluate flags against the live Flagship service. diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index f24d02f803..a043167bd8 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -34,6 +34,7 @@ import { BROWSER_RENDERING_PLUGIN_NAME, D1_PLUGIN_NAME, DURABLE_OBJECTS_PLUGIN_NAME, + FLAGSHIP_PLUGIN_NAME, getDirectSocketName, getGlobalServices, getPersistPath, @@ -148,6 +149,7 @@ import type { D1Database, DurableObjectNamespace, Fetcher, + Flags, ImagesBinding, KVNamespace, KVNamespaceListKey, @@ -2957,6 +2959,9 @@ export class Miniflare { }> { return this.#getProxy(HELLO_WORLD_PLUGIN_NAME, bindingName, workerName); } + getFlagshipBinding(bindingName: string, workerName?: string): Promise { + return this.#getProxy(FLAGSHIP_PLUGIN_NAME, bindingName, workerName); + } getStreamBinding( bindingName: string, workerName?: string diff --git a/packages/miniflare/src/plugins/flagship/index.ts b/packages/miniflare/src/plugins/flagship/index.ts new file mode 100644 index 0000000000..b675bf6bb8 --- /dev/null +++ b/packages/miniflare/src/plugins/flagship/index.ts @@ -0,0 +1,84 @@ +import BINDING_SCRIPT from "worker:flagship/binding"; +import { z } from "zod"; +import { + getUserBindingServiceName, + ProxyNodeBinding, + remoteProxyClientWorker, +} from "../shared"; +import type { Worker_Binding } from "../../runtime"; +import type { Plugin, RemoteProxyConnectionString } from "../shared"; + +const FlagshipSchema = z.object({ + app_id: z.string(), + remoteProxyConnectionString: z + .custom() + .optional(), +}); + +export const FlagshipOptionsSchema = z.object({ + flagship: z.record(FlagshipSchema).optional(), +}); + +export const FLAGSHIP_PLUGIN_NAME = "flagship"; + +export const FLAGSHIP_PLUGIN: Plugin = { + options: FlagshipOptionsSchema, + async getBindings(options) { + if (!options.flagship) { + return []; + } + + return Object.entries(options.flagship).map( + ([name, config]) => ({ + name, + service: { + name: getUserBindingServiceName( + FLAGSHIP_PLUGIN_NAME, + name, + config.remoteProxyConnectionString + ), + entrypoint: "FlagshipBinding", + }, + }) + ); + }, + getNodeBindings(options: z.infer) { + if (!options.flagship) { + return {}; + } + return Object.fromEntries( + Object.keys(options.flagship).map((name) => [ + name, + new ProxyNodeBinding(), + ]) + ); + }, + async getServices({ options }) { + if (!options.flagship) { + return []; + } + + return Object.entries(options.flagship).map( + ([name, { remoteProxyConnectionString }]) => { + return { + name: getUserBindingServiceName( + FLAGSHIP_PLUGIN_NAME, + name, + remoteProxyConnectionString + ), + worker: remoteProxyConnectionString + ? remoteProxyClientWorker(remoteProxyConnectionString, name) + : { + compatibilityDate: "2025-03-17", + modules: [ + { + name: "binding.worker.js", + esModule: BINDING_SCRIPT(), + }, + ], + }, + }; + } + ); + }, +}; diff --git a/packages/miniflare/src/plugins/index.ts b/packages/miniflare/src/plugins/index.ts index 961a999b3f..164447dd52 100644 --- a/packages/miniflare/src/plugins/index.ts +++ b/packages/miniflare/src/plugins/index.ts @@ -19,6 +19,7 @@ import { } from "./dispatch-namespace"; import { DURABLE_OBJECTS_PLUGIN, DURABLE_OBJECTS_PLUGIN_NAME } from "./do"; import { EMAIL_PLUGIN, EMAIL_PLUGIN_NAME } from "./email"; +import { FLAGSHIP_PLUGIN, FLAGSHIP_PLUGIN_NAME } from "./flagship"; import { HELLO_WORLD_PLUGIN, HELLO_WORLD_PLUGIN_NAME } from "./hello-world"; import { HYPERDRIVE_PLUGIN, HYPERDRIVE_PLUGIN_NAME } from "./hyperdrive"; import { IMAGES_PLUGIN, IMAGES_PLUGIN_NAME } from "./images"; @@ -74,6 +75,7 @@ export const PLUGINS = { [VPC_SERVICES_PLUGIN_NAME]: VPC_SERVICES_PLUGIN, [MTLS_PLUGIN_NAME]: MTLS_PLUGIN, [HELLO_WORLD_PLUGIN_NAME]: HELLO_WORLD_PLUGIN, + [FLAGSHIP_PLUGIN_NAME]: FLAGSHIP_PLUGIN, [WORKER_LOADER_PLUGIN_NAME]: WORKER_LOADER_PLUGIN, [MEDIA_PLUGIN_NAME]: MEDIA_PLUGIN, [VERSION_METADATA_PLUGIN_NAME]: VERSION_METADATA_PLUGIN, @@ -141,6 +143,7 @@ export type WorkerOptions = z.input & z.input & z.input & z.input & + z.input & z.input & z.input & z.input; @@ -230,6 +233,7 @@ export * from "./vpc-networks"; export * from "./vpc-services"; export * from "./mtls"; export * from "./hello-world"; +export * from "./flagship"; export * from "./worker-loader"; export * from "./media"; export * from "./version-metadata"; diff --git a/packages/miniflare/src/workers/flagship/binding.worker.ts b/packages/miniflare/src/workers/flagship/binding.worker.ts new file mode 100644 index 0000000000..c944577a7d --- /dev/null +++ b/packages/miniflare/src/workers/flagship/binding.worker.ts @@ -0,0 +1,104 @@ +// Local stub for Flagship feature flag binding. +// In local dev mode, all flag evaluations return the provided default value. +// Use `wrangler dev --remote` to evaluate flags against the real Flagship service. + +import { WorkerEntrypoint } from "cloudflare:workers"; + +interface EvaluationDetails { + flagKey: string; + value: T; + variant?: string; + reason?: string; + errorCode?: string; + errorMessage?: string; +} + +export class FlagshipBinding extends WorkerEntrypoint { + async get( + _flagKey: string, + defaultValue?: unknown, + _context?: Record + ): Promise { + return defaultValue; + } + + async getBooleanValue( + _flagKey: string, + defaultValue: boolean, + _context?: Record + ): Promise { + return defaultValue; + } + + async getStringValue( + _flagKey: string, + defaultValue: string, + _context?: Record + ): Promise { + return defaultValue; + } + + async getNumberValue( + _flagKey: string, + defaultValue: number, + _context?: Record + ): Promise { + return defaultValue; + } + + async getObjectValue( + _flagKey: string, + defaultValue: T, + _context?: Record + ): Promise { + return defaultValue; + } + + async getBooleanDetails( + flagKey: string, + defaultValue: boolean, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getStringDetails( + flagKey: string, + defaultValue: string, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getNumberDetails( + flagKey: string, + defaultValue: number, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } + + async getObjectDetails( + flagKey: string, + defaultValue: T, + _context?: Record + ): Promise> { + return { + flagKey, + value: defaultValue, + reason: "DEFAULT", + }; + } +} diff --git a/packages/miniflare/test/plugins/flagship/index.spec.ts b/packages/miniflare/test/plugins/flagship/index.spec.ts new file mode 100644 index 0000000000..db9c56620b --- /dev/null +++ b/packages/miniflare/test/plugins/flagship/index.spec.ts @@ -0,0 +1,354 @@ +import { Miniflare } from "miniflare"; +import { test } from "vitest"; +import { useDispose } from "../../test-shared"; + +test("flagship", async ({ expect }) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + + if (url.pathname === "/boolean") { + const value = await env.FLAGS.getBooleanValue("my-flag", false); + return Response.json({ value }); + } + + if (url.pathname === "/string") { + const value = await env.FLAGS.getStringValue("variant", "control", { + userId: "user-123", + }); + return Response.json({ value }); + } + + if (url.pathname === "/number") { + const value = await env.FLAGS.getNumberValue("rate-limit", 100); + return Response.json({ value }); + } + + if (url.pathname === "/details") { + const details = await env.FLAGS.getBooleanDetails("my-flag", true); + return Response.json(details); + } + + if (url.pathname === "/get") { + const value = await env.FLAGS.get("any-flag", "fallback"); + return Response.json({ value }); + } + + if (url.pathname === "/object") { + const value = await env.FLAGS.getObjectValue("config", { theme: "light", beta: false }); + return Response.json({ value }); + } + + if (url.pathname === "/string-details") { + const details = await env.FLAGS.getStringDetails("variant", "control"); + return Response.json(details); + } + + if (url.pathname === "/number-details") { + const details = await env.FLAGS.getNumberDetails("rate-limit", 100); + return Response.json(details); + } + + if (url.pathname === "/object-details") { + const details = await env.FLAGS.getObjectDetails("config", { theme: "light" }); + return Response.json(details); + } + + return new Response("Not found", { status: 404 }); + }, + } + `, + }); + useDispose(mf); + + // Local stub returns the default value for all evaluations + const boolRes = await mf.dispatchFetch("http://placeholder/boolean"); + expect(await boolRes.json()).toEqual({ value: false }); + + const strRes = await mf.dispatchFetch("http://placeholder/string"); + expect(await strRes.json()).toEqual({ value: "control" }); + + const numRes = await mf.dispatchFetch("http://placeholder/number"); + expect(await numRes.json()).toEqual({ value: 100 }); + + const detailsRes = await mf.dispatchFetch("http://placeholder/details"); + expect(await detailsRes.json()).toEqual({ + flagKey: "my-flag", + value: true, + reason: "DEFAULT", + }); + + const getRes = await mf.dispatchFetch("http://placeholder/get"); + expect(await getRes.json()).toEqual({ value: "fallback" }); + + const objRes = await mf.dispatchFetch("http://placeholder/object"); + expect(await objRes.json()).toEqual({ + value: { theme: "light", beta: false }, + }); + + const strDetailsRes = await mf.dispatchFetch( + "http://placeholder/string-details" + ); + expect(await strDetailsRes.json()).toEqual({ + flagKey: "variant", + value: "control", + reason: "DEFAULT", + }); + + const numDetailsRes = await mf.dispatchFetch( + "http://placeholder/number-details" + ); + expect(await numDetailsRes.json()).toEqual({ + flagKey: "rate-limit", + value: 100, + reason: "DEFAULT", + }); + + const objDetailsRes = await mf.dispatchFetch( + "http://placeholder/object-details" + ); + expect(await objDetailsRes.json()).toEqual({ + flagKey: "config", + value: { theme: "light" }, + reason: "DEFAULT", + }); +}); + +test("getObjectValue: returns default object value", async ({ expect }) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const value = await env.FLAGS.getObjectValue("config", { theme: "dark", features: ["a", "b"] }); + return Response.json(value); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ theme: "dark", features: ["a", "b"] }); +}); + +test("getObjectValue: returns default with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const value = await env.FLAGS.getObjectValue("config", { enabled: true }, { userId: "user-123" }); + return Response.json(value); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ enabled: true }); +}); + +test("getObjectDetails: returns evaluation details with default object", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getObjectDetails("app-config", { maxRetries: 3, timeout: 5000 }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "app-config", + value: { maxRetries: 3, timeout: 5000 }, + reason: "DEFAULT", + }); +}); + +test("getObjectDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getObjectDetails("config", { color: "blue" }, { region: "us-east" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "config", + value: { color: "blue" }, + reason: "DEFAULT", + }); +}); + +test("getStringDetails: returns evaluation details with default string", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getStringDetails("color-scheme", "dark-mode"); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "color-scheme", + value: "dark-mode", + reason: "DEFAULT", + }); +}); + +test("getStringDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getStringDetails("variant", "treatment-a", { userId: "user-456" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "variant", + value: "treatment-a", + reason: "DEFAULT", + }); +}); + +test("getNumberDetails: returns evaluation details with default number", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getNumberDetails("max-connections", 50); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "max-connections", + value: 50, + reason: "DEFAULT", + }); +}); + +test("getNumberDetails: returns evaluation details with context parameter", async ({ + expect, +}) => { + const mf = new Miniflare({ + compatibilityDate: "2025-01-01", + flagship: { + FLAGS: { + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + }, + modules: true, + script: ` + export default { + async fetch(request, env) { + const details = await env.FLAGS.getNumberDetails("timeout-ms", 3000, { tier: "premium" }); + return Response.json(details); + }, + } + `, + }); + useDispose(mf); + + const res = await mf.dispatchFetch("http://placeholder/"); + expect(await res.json()).toEqual({ + flagKey: "timeout-ms", + value: 3000, + reason: "DEFAULT", + }); +}); diff --git a/packages/workers-utils/src/config/config.ts b/packages/workers-utils/src/config/config.ts index e78ab96a6c..84139dd709 100644 --- a/packages/workers-utils/src/config/config.ts +++ b/packages/workers-utils/src/config/config.ts @@ -347,6 +347,7 @@ export const defaultWranglerConfig: Config = { media: undefined, version_metadata: undefined, unsafe_hello_world: [], + flagship: [], ratelimits: [], worker_loaders: [], diff --git a/packages/workers-utils/src/config/environment.ts b/packages/workers-utils/src/config/environment.ts index 276341ec0c..01a4361b5c 100644 --- a/packages/workers-utils/src/config/environment.ts +++ b/packages/workers-utils/src/config/environment.ts @@ -1349,6 +1349,26 @@ export interface EnvironmentNonInheritable { enable_timer?: boolean; }[]; + /** + * Specifies Flagship feature flag 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 + */ + flagship: { + /** The binding name used to refer to the bound Flagship service. */ + binding: string; + + /** The Flagship app ID to bind to. */ + app_id: string; + + /** Whether to use the remote Flagship service for flag evaluation in local dev. */ + remote?: boolean; + }[]; + /** * Specifies rate limit bindings that are bound to this Worker environment. * diff --git a/packages/workers-utils/src/config/validation.ts b/packages/workers-utils/src/config/validation.ts index 37e17f279b..b465ceefca 100644 --- a/packages/workers-utils/src/config/validation.ts +++ b/packages/workers-utils/src/config/validation.ts @@ -104,6 +104,7 @@ export type ConfigBindingFieldName = | "ratelimits" | "assets" | "unsafe_hello_world" + | "flagship" | "worker_loaders" | "vpc_services" | "vpc_networks"; @@ -144,6 +145,7 @@ export const friendlyBindingNames: Record = { ratelimits: "Rate Limit", assets: "Assets", unsafe_hello_world: "Hello World", + flagship: "Flagship", worker_loaders: "Worker Loader", vpc_services: "VPC Service", vpc_networks: "VPC Network", @@ -186,6 +188,7 @@ const bindingTypeFriendlyNames: Record = { secrets_store_secret: "Secrets Store Secret", logfwdr: "logfwdr", unsafe_hello_world: "Hello World", + flagship: "Flagship", ratelimit: "Rate Limit", worker_loader: "Worker Loader", vpc_service: "VPC Service", @@ -1884,6 +1887,16 @@ function normalizeAndValidateEnvironment( validateBindingArray(envName, validateHelloWorldBinding), [] ), + flagship: notInheritable( + diagnostics, + topLevelEnv, + rawConfig, + rawEnv, + envName, + "flagship", + validateBindingArray(envName, validateFlagshipBinding), + [] + ), worker_loaders: notInheritable( diagnostics, topLevelEnv, @@ -2979,6 +2992,7 @@ const validateUnsafeBinding: ValidatorFn = (diagnostics, field, value) => { "pipeline", "worker_loader", "vpc_service", + "flagship", "vpc_network", "stream", "media", @@ -4781,6 +4795,44 @@ const validateHelloWorldBinding: ValidatorFn = (diagnostics, field, value) => { return isValid; }; +const validateFlagshipBinding: ValidatorFn = (diagnostics, field, value) => { + if (typeof value !== "object" || value === null) { + diagnostics.errors.push( + `"flagship" 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, "app_id", "string")) { + diagnostics.errors.push( + `"${field}" bindings must have a string "app_id" field but got ${JSON.stringify( + value + )}.` + ); + isValid = false; + } + + validateAdditionalProperties(diagnostics, field, Object.keys(value), [ + "binding", + "app_id", + "remote", + ]); + + if (!isRemoteValid(value, field, diagnostics)) { + isValid = false; + } + + return isValid; +}; + const validateWorkerLoaderBinding: ValidatorFn = ( diagnostics, field, diff --git a/packages/workers-utils/src/map-worker-metadata-bindings.ts b/packages/workers-utils/src/map-worker-metadata-bindings.ts index ccd7462f41..2a3d35f5ce 100644 --- a/packages/workers-utils/src/map-worker-metadata-bindings.ts +++ b/packages/workers-utils/src/map-worker-metadata-bindings.ts @@ -142,6 +142,16 @@ export function mapWorkerMetadataBindings( ]; break; } + case "flagship": { + configObj.flagship = [ + ...(configObj.flagship ?? []), + { + binding: binding.name, + app_id: binding.app_id, + }, + ]; + break; + } case "service": { configObj.services = [ diff --git a/packages/workers-utils/src/types.ts b/packages/workers-utils/src/types.ts index 143ec41b54..29fbf67289 100644 --- a/packages/workers-utils/src/types.ts +++ b/packages/workers-utils/src/types.ts @@ -16,6 +16,7 @@ import type { CfDispatchNamespace, CfDurableObject, CfDurableObjectMigrations, + CfFlagship, CfHelloWorld, CfHyperdrive, CfImagesBinding, @@ -159,6 +160,11 @@ export type WorkerMetadataBinding = name: string; enable_timer?: boolean; } + | { + type: "flagship"; + name: string; + app_id: string; + } | { type: "ratelimit"; name: string; @@ -332,6 +338,7 @@ export type Binding = | ({ type: "secrets_store_secret" } & BindingOmit) | ({ type: "logfwdr" } & NameOmit) | ({ type: "unsafe_hello_world" } & BindingOmit) + | ({ type: "flagship" } & BindingOmit) | ({ type: "ratelimit" } & NameOmit) | ({ type: "worker_loader" } & BindingOmit) | ({ type: "vpc_service" } & BindingOmit) diff --git a/packages/workers-utils/src/worker.ts b/packages/workers-utils/src/worker.ts index f7cc525212..b57bb93e74 100644 --- a/packages/workers-utils/src/worker.ts +++ b/packages/workers-utils/src/worker.ts @@ -253,6 +253,12 @@ export interface CfHelloWorld { enable_timer?: boolean; } +export interface CfFlagship { + binding: string; + app_id: string; + remote?: boolean; +} + export interface CfWorkerLoader { binding: string; } 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 7b99e91245..8ef8b2f79b 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 @@ -75,6 +75,7 @@ describe("normalizeAndValidateConfig()", () => { r2_buckets: [], secrets_store_secrets: [], unsafe_hello_world: [], + flagship: [], ratelimits: [], vpc_services: [], vpc_networks: [], @@ -4703,6 +4704,713 @@ describe("normalizeAndValidateConfig()", () => { }); }); + describe("[flagship]", () => { + it("should error if flagship is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got {}." + `); + }); + + it("should error if flagship is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: "bad" }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got "bad"." + `); + }); + + it("should error if flagship is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: 999 }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got 999." + `); + }); + + it("should error if flagship is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { flagship: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "flagship" should be an array but got null." + `); + }); + + it("should accept valid flagship bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + { + binding: "FLAGS", + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if flagship bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + // @ts-expect-error purposely using an invalid value + {}, + // @ts-expect-error purposely using an invalid value + { binding: "VALID" }, + // @ts-expect-error purposely using an invalid value + { binding: 2000, app_id: 2111 }, + { + binding: "BINDING_2", + app_id: "valid-app-id", + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "flagship[0]" bindings must have a string "binding" field but got {}. + - "flagship[0]" bindings must have a string "app_id" field but got {}. + - "flagship[1]" bindings must have a string "app_id" field but got {"binding":"VALID"}. + - "flagship[2]" bindings must have a string "binding" field but got {"binding":2000,"app_id":2111}. + - "flagship[2]" bindings must have a string "app_id" field but got {"binding":2000,"app_id":2111}." + `); + }); + + it("should warn on unexpected fields", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + flagship: [ + { + binding: "FLAGS", + app_id: "valid-app-id", + // @ts-expect-error purposely using an invalid field + unknown_field: true, + }, + ], + }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in flagship[0] field: "unknown_field"" + `); + }); + }); + + describe("[workflows]", () => { + it("should error if workflows is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: {} }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got {}." + `); + }); + + it("should error if workflows is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: "BAD" }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got "BAD"." + `); + }); + + it("should error if workflows is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: 999 }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got 999." + `); + }); + + it("should error if workflows is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + // @ts-expect-error purposely using an invalid value + { workflows: null }, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "workflows" should be an array but got null." + `); + }); + + it("should accept valid workflow bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should accept valid workflow bindings with optional fields", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + script_name: "my-script", + remote: true, + limits: { steps: 100 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if workflow bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + {}, + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + }, + { binding: 2000, name: 2111, class_name: 3000 }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should have a string "binding" field but got {}. + - "workflows[0]" bindings should have a string "name" field but got {}. + - "workflows[0]" bindings should have a string "class_name" field but got {}. + - "workflows[2]" bindings should have a string "binding" field but got {"binding":2000,"name":2111,"class_name":3000}. + - "workflows[2]" bindings should have a string "name" field but got {"binding":2000,"name":2111,"class_name":3000}. + - "workflows[2]" bindings should have a string "class_name" field but got {"binding":2000,"name":2111,"class_name":3000}." + `); + }); + + it("should error if workflow name has invalid format", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "invalid name with spaces", + class_name: "MyWorkflow", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" binding "name" field is invalid. Workflow names must be 1-64 characters long, start with a letter, number, or underscore, and may only contain letters, numbers, underscores, or hyphens." + `); + }); + + it("should error if optional fields have wrong types", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + script_name: 123, + remote: "yes", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should, optionally, have a string "script_name" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","script_name":123,"remote":"yes"}. + - "workflows[0]" bindings should, optionally, have a boolean "remote" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","script_name":123,"remote":"yes"}." + `); + }); + + it("should error if limits is not an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: "bad", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings should, optionally, have an object "limits" field but got {"binding":"MY_WORKFLOW","name":"my-workflow","class_name":"MyWorkflow","limits":"bad"}." + `); + }); + + it("should error if limits.steps is not a positive integer", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: { steps: -1 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" bindings "limits.steps" field must be a positive integer but got -1." + `); + }); + + it("should warn if limits.steps exceeds 25000", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + limits: { steps: 30000 }, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows[0]" has a step limit of 30000, which exceeds the production maximum of 25,000. This configuration may not work when deployed." + `); + }); + + it("should warn on unexpected fields", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "MY_WORKFLOW", + name: "my-workflow", + class_name: "MyWorkflow", + unknown_field: true, + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - Unexpected fields found in workflows[0] field: "unknown_field"" + `); + }); + + it("should error on duplicate workflow names", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + workflows: [ + { + binding: "WORKFLOW_1", + name: "my-workflow", + class_name: "MyWorkflow1", + }, + { + binding: "WORKFLOW_2", + name: "my-workflow", + class_name: "MyWorkflow2", + }, + ], + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "workflows" bindings must have unique "name" values; duplicate(s) found: "my-workflow"" + `); + }); + }); + + describe("[logfwdr]", () => { + it("should error if logfwdr is an array", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: [] } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got []." + `); + }); + + it("should error if logfwdr is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: "BAD" } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got "BAD"." + `); + }); + + it("should error if logfwdr is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: 999 } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got 999." + `); + }); + + it("should error if logfwdr is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: null } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" should be an object but got null." + `); + }); + + it("should error if logfwdr.bindings is not defined", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: {} } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr" is missing the required "bindings" property." + `); + }); + + it("should error if logfwdr.bindings is an object", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: {} } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got {}." + `); + }); + + it("should error if logfwdr.bindings is a string", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: "BAD" } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got "BAD"." + `); + }); + + it("should error if logfwdr.bindings is a number", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: 999 } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got 999." + `); + }); + + it("should error if logfwdr.bindings is null", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { logfwdr: { bindings: null } } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - The field "logfwdr.bindings" should be an array but got null." + `); + }); + + it("should accept valid logfwdr bindings", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(false); + }); + + it("should error if logfwdr.bindings are not valid", ({ expect }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + {}, + { name: "VALID", destination: "valid-dest" }, + { name: 123, destination: 456 }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasWarnings()).toBe(false); + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + + - "logfwdr.bindings[0]": {} + - binding should have a string "name" field. + - binding should have a string "destination" field. + + - "logfwdr.bindings[2]": {"name":123,"destination":456} + - binding should have a string "name" field. + - binding should have a string "destination" field." + `); + }); + + it("should error if logfwdr has deprecated schema property", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + }, + ], + schema: "some-schema", + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(true); + expect(diagnostics.renderErrors()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + - "logfwdr" binding "schema" property has been replaced with the "unsafe.capnp" object, which expects a "base_path" and an array of "source_schemas" to compile, or a "compiled_schema" property." + `); + }); + + it("should warn on unexpected fields in logfwdr bindings", ({ + expect, + }) => { + const { diagnostics } = normalizeAndValidateConfig( + { + logfwdr: { + bindings: [ + { + name: "httplogs", + destination: "httplogs", + unknown_field: true, + }, + ], + }, + } as unknown as RawConfig, + undefined, + undefined, + { env: undefined } + ); + + expect(diagnostics.hasErrors()).toBe(false); + expect(diagnostics.hasWarnings()).toBe(true); + expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(` + "Processing wrangler configuration: + + - "logfwdr.bindings[0]": {"name":"httplogs","destination":"httplogs","unknown_field":true} + - Unexpected fields found in logfwdr.bindings[0] field: "unknown_field"" + `); + }); + }); + describe("[ratelimit]", () => { it("should error if ratelimit is an object", ({ expect }) => { const { diagnostics } = normalizeAndValidateConfig( diff --git a/packages/wrangler/src/__tests__/deploy/core.test.ts b/packages/wrangler/src/__tests__/deploy/core.test.ts index 1d19733b76..ae0a92cad5 100644 --- a/packages/wrangler/src/__tests__/deploy/core.test.ts +++ b/packages/wrangler/src/__tests__/deploy/core.test.ts @@ -726,7 +726,7 @@ describe("deploy", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Worker Startup Time: 100 ms @@ -772,7 +772,7 @@ describe("deploy", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in. Total Upload: xx KiB / gzip: xx KiB Worker Startup Time: 100 ms diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 272f665ee8..a9cf718677 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -416,6 +416,12 @@ const bindingsConfigMock: Omit< enable_timer: true, }, ], + flagship: [ + { + binding: "FLAGS", + app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + }, + ], services: [ { binding: "SERVICE_BINDING", service: "service_name" }, { @@ -765,6 +771,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flagship; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; @@ -880,6 +887,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flagship; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; @@ -1058,6 +1066,7 @@ describe("generate types", () => { TEST_QUEUE_BINDING: Queue; SECRET: SecretsStoreSecret; HELLO_WORLD: HelloWorldBinding; + FLAGS: Flagship; RATE_LIMITER: RateLimit; WORKER_LOADER_BINDING: WorkerLoader; VPC_SERVICE_BINDING: Fetcher; diff --git a/packages/wrangler/src/__tests__/user.test.ts b/packages/wrangler/src/__tests__/user.test.ts index 55e3f1e6ed..d969aac6bf 100644 --- a/packages/wrangler/src/__tests__/user.test.ts +++ b/packages/wrangler/src/__tests__/user.test.ts @@ -80,7 +80,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -126,7 +126,7 @@ describe("User", () => { Temporary login server listening on 0.0.0.0:8976 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -172,7 +172,7 @@ describe("User", () => { Temporary login server listening on mylocalhost.local:8976 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -218,7 +218,7 @@ describe("User", () => { Temporary login server listening on localhost:8787 Note that the OAuth login page will always redirect to \`localhost:8976\`. If you have changed the callback host or port because you are running in a container, then ensure that you have port forwarding set up correctly. - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(readAuthConfigFile()).toEqual({ @@ -260,7 +260,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=4b2ea6cc-9421-4761-874b-ce550e0e3def&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.staging.cloudflare.com/oauth2/auth?response_type=code&client_id=4b2ea6cc-9421-4761-874b-ce550e0e3def&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); @@ -389,7 +389,7 @@ describe("User", () => { ⛅️ wrangler x.x.x ────────────────── Attempting to login via OAuth... - Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 + Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth?response_type=code&client_id=54d11594-84e4-41aa-b438-e81b8fa78ee7&redirect_uri=http%3A%2F%2Flocalhost%3A8976%2Foauth%2Fcallback&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20ai%3Awrite%20ai-search%3Awrite%20ai-search%3Arun%20queues%3Awrite%20pipelines%3Awrite%20secrets_store%3Awrite%20flagship%3Aread%20flagship%3Awrite%20containers%3Awrite%20cloudchamber%3Awrite%20connectivity%3Aadmin%20email_routing%3Awrite%20email_sending%3Awrite%20offline_access&state=MOCK_STATE_PARAM&code_challenge=MOCK_CODE_CHALLENGE&code_challenge_method=S256 Successfully logged in." `); expect(std.warn).toMatchInlineSnapshot(`""`); diff --git a/packages/wrangler/src/__tests__/whoami.test.ts b/packages/wrangler/src/__tests__/whoami.test.ts index 96623cb0f6..9afc135223 100644 --- a/packages/wrangler/src/__tests__/whoami.test.ts +++ b/packages/wrangler/src/__tests__/whoami.test.ts @@ -341,6 +341,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin @@ -406,6 +408,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin @@ -517,6 +521,8 @@ describe("whoami", () => { - queues:write - pipelines:write - secrets_store:write + - flagship:read + - flagship:write - containers:write - cloudchamber:write - connectivity:admin diff --git a/packages/wrangler/src/api/startDevWorker/utils.ts b/packages/wrangler/src/api/startDevWorker/utils.ts index bf195fceb4..66e8385aa0 100644 --- a/packages/wrangler/src/api/startDevWorker/utils.ts +++ b/packages/wrangler/src/api/startDevWorker/utils.ts @@ -355,6 +355,12 @@ export function convertConfigToBindings( } break; } + case "flagship": { + for (const { binding, ...x } of info) { + output[binding] = { type: "flagship", ...x }; + } + break; + } case "ratelimits": { for (const { name, ...x } of info) { output[name] = { type: "ratelimit", ...x }; diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index 3ccc333da8..ec02d5fa4e 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -33,6 +33,7 @@ const reorderableBindings = { ratelimits: true, analytics_engine_datasets: true, unsafe_hello_world: true, + flagship: true, worker_loaders: true, vpc_services: true, vpc_networks: true, @@ -235,6 +236,12 @@ function removeRemoteConfigFieldFromBindings(normalizedConfig: Config): void { ); } + if (normalizedConfig.flagship?.length) { + normalizedConfig.flagship = normalizedConfig.flagship.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 ae704c8708..1fa2be7b2d 100644 --- a/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts +++ b/packages/wrangler/src/deployment-bundle/create-worker-upload-form.ts @@ -140,6 +140,7 @@ export function createWorkerUploadForm( "unsafe_hello_world", bindings ); + const flagship = extractBindingsOfType("flagship", bindings); const ratelimits = extractBindingsOfType("ratelimit", bindings); const vpc_services = extractBindingsOfType("vpc_service", bindings); const vpc_networks = extractBindingsOfType("vpc_network", bindings); @@ -384,6 +385,14 @@ export function createWorkerUploadForm( }); }); + flagship.forEach(({ binding, app_id }) => { + metadataBindings.push({ + name: binding, + type: "flagship", + app_id, + }); + }); + ratelimits.forEach(({ name, namespace_id, simple }) => { metadataBindings.push({ name, diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index d0d9e8d4b7..48eb3d7780 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -422,6 +422,7 @@ type WorkerOptionsBindings = Pick< | "dispatchNamespaces" | "mtlsCertificates" | "helloWorld" + | "flagship" | "workerLoaders" | "unsafeBindings" | "additionalUnboundDurableObjects" @@ -494,6 +495,7 @@ export function buildMiniflareBindingOptions( "unsafe_hello_world", bindings ); + const flagshipBindings = extractBindingsOfType("flagship", bindings); const workerLoaders = extractBindingsOfType("worker_loader", bindings); const sendEmailBindings = extractBindingsOfType("send_email", bindings); // Extract both regular and unsafe ratelimit bindings @@ -783,6 +785,18 @@ export function buildMiniflareBindingOptions( helloWorld: Object.fromEntries( helloWorldBindings.map((binding) => [binding.binding, binding]) ), + flagship: Object.fromEntries( + flagshipBindings.map((binding) => [ + binding.binding, + { + app_id: binding.app_id, + remoteProxyConnectionString: + binding.remote && remoteProxyConnectionString + ? remoteProxyConnectionString + : undefined, + }, + ]) + ), workerLoaders: Object.fromEntries( workerLoaders.map(({ binding }) => [binding, {}]) ), diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index 71d09edd04..162dbcb7a7 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -1819,6 +1819,21 @@ function collectCoreBindings( ); } + for (const [index, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { + throwMissingBindingError({ + binding: flagshipBinding, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + addBinding(flagshipBinding.binding, "Flagship", "flagship", envName); + } + for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { if (!ratelimit.name) { throwMissingBindingError({ @@ -2782,6 +2797,25 @@ function collectCoreBindingsPerEnvironment( }); } + for (const [index, flagshipBinding] of (env.flagship ?? []).entries()) { + if (!flagshipBinding.binding) { + throwMissingBindingError({ + binding: flagshipBinding, + bindingType: "flagship", + configPath: args.config, + envName, + fieldName: "binding", + index, + }); + } + + bindings.push({ + bindingCategory: "flagship", + name: flagshipBinding.binding, + type: "Flagship", + }); + } + for (const [index, ratelimit] of (env.ratelimits ?? []).entries()) { if (!ratelimit.name) { throwMissingBindingError({ diff --git a/packages/wrangler/src/user/user.ts b/packages/wrangler/src/user/user.ts index 9c2f6b126f..49576f2ddf 100644 --- a/packages/wrangler/src/user/user.ts +++ b/packages/wrangler/src/user/user.ts @@ -374,6 +374,8 @@ const DefaultScopes = { "See and change Cloudflare Pipelines configurations and data", "secrets_store:write": "See and change secrets + stores within the Secrets Store", + "flagship:read": "See Flagship feature flags and apps", + "flagship:write": "See and change Flagship feature flags and apps", "containers:write": "Manage Workers Containers", "cloudchamber:write": "Manage Cloudchamber", "connectivity:admin": diff --git a/packages/wrangler/src/utils/add-created-resource-config.ts b/packages/wrangler/src/utils/add-created-resource-config.ts index 3c45f89c71..b36b241cf7 100644 --- a/packages/wrangler/src/utils/add-created-resource-config.ts +++ b/packages/wrangler/src/utils/add-created-resource-config.ts @@ -42,6 +42,7 @@ type ValidKeys = Exclude< | "dispatch_namespaces" | "secrets_store_secrets" | "unsafe_hello_world" + | "flagship" >; export const sharedResourceCreationArgs = { diff --git a/packages/wrangler/src/utils/print-bindings.ts b/packages/wrangler/src/utils/print-bindings.ts index 659cbbd877..43405f10b8 100644 --- a/packages/wrangler/src/utils/print-bindings.ts +++ b/packages/wrangler/src/utils/print-bindings.ts @@ -136,6 +136,7 @@ export function printBindings( "unsafe_hello_world", bindings ); + const flagship = extractBindingsOfType("flagship", bindings); const media = extractBindingsOfType("media", bindings); const worker_loaders = extractBindingsOfType("worker_loader", bindings); @@ -458,6 +459,21 @@ export function printBindings( ); } + if (flagship.length > 0) { + output.push( + ...flagship.map(({ binding, app_id, remote }) => { + return { + name: binding, + type: getBindingTypeFriendlyName("flagship"), + value: app_id, + mode: getMode({ + isSimulatedLocally: context.remoteBindingsDisabled || !remote, + }), + }; + }) + ); + } + if (services.length > 0) { output.push( ...services.map(({ binding, service, entrypoint, remote }) => {