diff --git a/.changeset/c3-frameworks-update-12717.md b/.changeset/c3-frameworks-update-12717.md new file mode 100644 index 000000000000..a2b988fd0713 --- /dev/null +++ b/.changeset/c3-frameworks-update-12717.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ------------- | ----- | ----- | +| create-analog | 2.2.3 | 2.3.1 | diff --git a/.changeset/c3-frameworks-update-12718.md b/.changeset/c3-frameworks-update-12718.md new file mode 100644 index 000000000000..ac9aa501f835 --- /dev/null +++ b/.changeset/c3-frameworks-update-12718.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ---------- | ------ | ------ | +| sv | 0.12.2 | 0.12.4 | diff --git a/.changeset/c3-frameworks-update-12720.md b/.changeset/c3-frameworks-update-12720.md new file mode 100644 index 000000000000..a5227fc9e029 --- /dev/null +++ b/.changeset/c3-frameworks-update-12720.md @@ -0,0 +1,11 @@ +--- +"create-cloudflare": patch +--- + +Update dependencies of "create-cloudflare" + +The following dependency versions have been updated: + +| Dependency | From | To | +| ----------- | ---------------------- | ---------------------- | +| create-waku | 0.12.5-1.0.0-alpha.4-0 | 0.12.5-1.0.0-alpha.5-0 | diff --git a/.changeset/polite-schools-buy.md b/.changeset/polite-schools-buy.md new file mode 100644 index 000000000000..e81f0f465d7e --- /dev/null +++ b/.changeset/polite-schools-buy.md @@ -0,0 +1,7 @@ +--- +"create-cloudflare": patch +--- + +Update SolidStart template for compatibility with v2. + +SolidStart v2 uses the `nitro` Vite plugin so we now update the Nitro config in `vite.config.ts` rather than `app.config.ts`. diff --git a/.changeset/remove-ai-search-workaround.md b/.changeset/remove-ai-search-workaround.md new file mode 100644 index 000000000000..5ca198f5f29c --- /dev/null +++ b/.changeset/remove-ai-search-workaround.md @@ -0,0 +1,6 @@ +--- +"miniflare": patch +"wrangler": patch +--- + +Remove temporary AI Search RPC workaround (no user-facing changes) diff --git a/.changeset/sharp-sheep-buy.md b/.changeset/sharp-sheep-buy.md new file mode 100644 index 000000000000..206121c18a60 --- /dev/null +++ b/.changeset/sharp-sheep-buy.md @@ -0,0 +1,7 @@ +--- +"wrangler": minor +--- + +Add type generation support for experimental `secrets` property. + +This has precedence over deriving secret types from .env and .dev.vars files. diff --git a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts index 3053d857b7f5..fbf7dadc4104 100644 --- a/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts +++ b/packages/create-cloudflare/e2e/tests/frameworks/test-config.ts @@ -813,6 +813,8 @@ function getExperimentalFrameworkTestConfig( }, { name: "solid", + // quarantined: SolidStart moved from app.config to vite.config with Nitro plugin + quarantine: true, promptHandlers: [ { matcher: /Which template would you like to use/, diff --git a/packages/create-cloudflare/src/frameworks/package.json b/packages/create-cloudflare/src/frameworks/package.json index f68ceabe38fa..d0e16054b408 100644 --- a/packages/create-cloudflare/src/frameworks/package.json +++ b/packages/create-cloudflare/src/frameworks/package.json @@ -6,7 +6,7 @@ ], "dependencies": { "create-astro": "4.13.2", - "create-analog": "2.2.3", + "create-analog": "2.3.1", "@angular/create": "21.1.4", "create-docusaurus": "3.9.2", "create-hono": "0.19.4", @@ -18,10 +18,10 @@ "create-solid": "0.6.13", "create-vike": "0.0.581", "create-vue": "3.21.1", - "create-waku": "0.12.5-1.0.0-alpha.4-0", + "create-waku": "0.12.5-1.0.0-alpha.5-0", "@tanstack/create-start": "0.59.8", "gatsby": "5.16.1", - "sv": "0.12.2", + "sv": "0.12.4", "nuxi": "3.33.1" } } diff --git a/packages/create-cloudflare/templates/solid/c3.ts b/packages/create-cloudflare/templates/solid/c3.ts index 3143dfac159c..3cdc1d44dfd6 100644 --- a/packages/create-cloudflare/templates/solid/c3.ts +++ b/packages/create-cloudflare/templates/solid/c3.ts @@ -1,6 +1,5 @@ import { logRaw, updateStatus } from "@cloudflare/cli"; import { blue } from "@cloudflare/cli/colors"; -import { getLocalWorkerdCompatibilityDate } from "@cloudflare/workers-utils"; import { runFrameworkGenerator } from "frameworks/index"; import { mergeObjectProperties, transformFile } from "helpers/codemod"; import { usesTypescript } from "helpers/files"; @@ -21,42 +20,32 @@ const generate = async (ctx: C3Context) => { const configure = async (ctx: C3Context) => { usesTypescript(ctx); - const filePath = `app.config.${usesTypescript(ctx) ? "ts" : "js"}`; - - const { date: compatDate } = getLocalWorkerdCompatibilityDate({ - projectPath: ctx.project.path, - }); + const filePath = `vite.config.${usesTypescript(ctx) ? "ts" : "js"}`; updateStatus(`Updating configuration in ${blue(filePath)}`); transformFile(filePath, { visitCallExpression: function (n) { const callee = n.node.callee as recast.types.namedTypes.Identifier; - if (callee.name !== "defineConfig") { + if (callee.name !== "nitro") { return this.traverse(n); } const b = recast.types.builders; - mergeObjectProperties( - n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, - [ - b.objectProperty( - b.identifier("server"), - b.objectExpression([ - // preset: "cloudflare_module" - b.objectProperty( - b.identifier("preset"), - b.stringLiteral("cloudflare_module"), - ), - b.objectProperty( - b.identifier("compatibilityDate"), - b.stringLiteral(compatDate), - ), - ]), - ), - ], + const presetProp = b.objectProperty( + b.identifier("preset"), + b.stringLiteral("cloudflare-module"), ); + if (n.node.arguments.length === 0) { + n.node.arguments.push(b.objectExpression([presetProp])); + } else { + mergeObjectProperties( + n.node.arguments[0] as recast.types.namedTypes.ObjectExpression, + [presetProp], + ); + } + return false; }, }); diff --git a/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc b/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc index 43ba70955a16..a45f92a92fc4 100644 --- a/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc +++ b/packages/create-cloudflare/templates/solid/templates/wrangler.jsonc @@ -1,14 +1,9 @@ { "name": "", - "main": "./.output/server/index.mjs", "compatibility_date": "", "compatibility_flags": [ "nodejs_compat" ], - "assets": { - "binding": "ASSETS", - "directory": "./.output/public" - }, "observability": { "enabled": true } diff --git a/packages/miniflare/package.json b/packages/miniflare/package.json index ab2feacbf632..5b87cb4044b4 100644 --- a/packages/miniflare/package.json +++ b/packages/miniflare/package.json @@ -50,7 +50,7 @@ "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "catalog:default", - "workerd": "1.20260226.1", + "workerd": "1.20260301.1", "ws": "catalog:default", "youch": "4.1.0-beta.10" }, diff --git a/packages/miniflare/src/plugins/ai/index.ts b/packages/miniflare/src/plugins/ai/index.ts index 0c5c73bacf18..ee0f6bd1a230 100644 --- a/packages/miniflare/src/plugins/ai/index.ts +++ b/packages/miniflare/src/plugins/ai/index.ts @@ -70,8 +70,7 @@ export const AI_PLUGIN: Plugin = { ), worker: remoteProxyClientWorker( options.ai.remoteProxyConnectionString, - options.ai.binding, - "ai" + options.ai.binding ), }, ]; diff --git a/packages/miniflare/src/plugins/dispatch-namespace/index.ts b/packages/miniflare/src/plugins/dispatch-namespace/index.ts index dab9ada5cfb5..e9b224ee8e0b 100644 --- a/packages/miniflare/src/plugins/dispatch-namespace/index.ts +++ b/packages/miniflare/src/plugins/dispatch-namespace/index.ts @@ -90,7 +90,6 @@ export const DISPATCH_NAMESPACE_PLUGIN: Plugin< worker: remoteProxyClientWorker( config.remoteProxyConnectionString, name, - undefined, SCRIPT_DISPATCH_NAMESPACE_PROXY ), })); diff --git a/packages/miniflare/src/plugins/shared/constants.ts b/packages/miniflare/src/plugins/shared/constants.ts index 3a9158a42ead..475fbf68d2de 100644 --- a/packages/miniflare/src/plugins/shared/constants.ts +++ b/packages/miniflare/src/plugins/shared/constants.ts @@ -76,7 +76,6 @@ export function objectEntryWorker( export function remoteProxyClientWorker( remoteProxyConnectionString: RemoteProxyConnectionString | undefined, binding: string, - bindingType?: string, script?: () => string ) { return { @@ -100,14 +99,6 @@ export function remoteProxyClientWorker( name: "binding", text: binding, }, - ...(bindingType - ? [ - { - name: "bindingType", - text: bindingType, - }, - ] - : []), ], }; } diff --git a/packages/miniflare/src/workers/shared/remote-bindings-utils.ts b/packages/miniflare/src/workers/shared/remote-bindings-utils.ts index d780671e8171..175540da276f 100644 --- a/packages/miniflare/src/workers/shared/remote-bindings-utils.ts +++ b/packages/miniflare/src/workers/shared/remote-bindings-utils.ts @@ -6,12 +6,10 @@ import { newWebSocketRpcSession } from "capnweb"; export type RemoteBindingEnv = { remoteProxyConnectionString?: string; binding: string; - bindingType?: string; }; /** Headers sent alongside proxy requests to provide additional context. */ export type ProxyMetadata = { - "MF-Binding-Type"?: string; "MF-Dispatch-Namespace-Options"?: string; }; diff --git a/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts b/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts index c5b6cee08db7..18af7ce58679 100644 --- a/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts +++ b/packages/miniflare/src/workers/shared/remote-proxy-client.worker.ts @@ -19,11 +19,7 @@ export default class Client extends WorkerEntrypoint { super(ctx, env); const stub = env.remoteProxyConnectionString - ? makeRemoteProxyStub( - env.remoteProxyConnectionString, - env.binding, - env.bindingType ? { "MF-Binding-Type": env.bindingType } : undefined - ) + ? makeRemoteProxyStub(env.remoteProxyConnectionString, env.binding) : undefined; return new Proxy(this, { diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index c2fbe1066ece..728e1c7256a2 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -73,7 +73,7 @@ "miniflare": "workspace:*", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", - "workerd": "1.20260226.1" + "workerd": "1.20260301.1" }, "devDependencies": { "@aws-sdk/client-s3": "^3.721.0", diff --git a/packages/wrangler/src/__tests__/dev/proxy-server-ai-rpc.test.ts b/packages/wrangler/src/__tests__/dev/proxy-server-ai-rpc.test.ts deleted file mode 100644 index 89c8b73fd8ee..000000000000 --- a/packages/wrangler/src/__tests__/dev/proxy-server-ai-rpc.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { describe, it, vi } from "vitest"; - -/** - * Tests for the AI RPC method wrapping logic in ProxyServerWorker. - * - * The raw AI binding (deployed with raw:true) has a workerd-internal prototype - * that capnweb classifies as "unsupported", causing - * "RPC stub points at a non-serializable type". - * - * The fix uses MF-Binding-Type (threaded from the miniflare AI plugin through - * the remote-proxy-client WebSocket URL) to identify AI bindings, then wraps - * them in a plain object delegating only the allowed RPC methods. - */ - -// Mirrors the constant from ProxyServerWorker.ts -const AI_RPC_METHODS = ["aiSearch"] as const; - -/** - * Re-implementation of the AI wrapping logic from - * ProxyServerWorker.ts getExposedJSRPCBinding() so we can unit-test it - * without pulling in cloudflare:email / capnweb. - */ -function wrapIfAiBinding( - bindingType: string | null, - targetBinding: object -): unknown { - if (bindingType === "ai") { - const wrapper: Record unknown> = {}; - for (const method of AI_RPC_METHODS) { - if ( - typeof (targetBinding as Record)[method] === "function" - ) { - wrapper[method] = (...args: unknown[]) => - (targetBinding as Record unknown>)[ - method - ](...args); - } - } - if (Object.keys(wrapper).length > 0) { - return wrapper; - } - } - return targetBinding; -} - -describe("ProxyServerWorker AI RPC wrapping", () => { - it("wraps an AI binding into a plain object", ({ expect }) => { - const binding = { aiSearch: vi.fn(), fetch: vi.fn() }; - - const wrapped = wrapIfAiBinding("ai", binding); - - expect(Object.getPrototypeOf(wrapped)).toBe(Object.prototype); - expect(wrapped).not.toBe(binding); - }); - - it("delegates aiSearch calls to the underlying binding", async ({ - expect, - }) => { - const mockAiSearch = vi.fn().mockResolvedValue({ result: "ok" }); - const binding = { aiSearch: mockAiSearch }; - - const wrapped = wrapIfAiBinding("ai", binding) as Record< - string, - (...args: unknown[]) => unknown - >; - - const params = { query: "test" }; - const result = await wrapped.aiSearch(params); - - expect(mockAiSearch).toHaveBeenCalledWith(params); - expect(result).toEqual({ result: "ok" }); - }); - - it("forwards all arguments to the underlying aiSearch method", ({ - expect, - }) => { - const mockAiSearch = vi.fn(); - const binding = { aiSearch: mockAiSearch }; - const wrapped = wrapIfAiBinding("ai", binding) as Record< - string, - (...args: unknown[]) => unknown - >; - - wrapped.aiSearch("arg1", "arg2", { nested: true }); - - expect(mockAiSearch).toHaveBeenCalledWith("arg1", "arg2", { - nested: true, - }); - }); - - it("does not wrap bindings without the ai binding type", ({ expect }) => { - const binding = { aiSearch: vi.fn(), otherMethod: vi.fn() }; - - const result = wrapIfAiBinding(null, binding); - - expect(result).toBe(binding); - }); - - it("does not wrap a service binding even if it has aiSearch", ({ - expect, - }) => { - const binding = { aiSearch: vi.fn(), otherMethod: vi.fn() }; - - const result = wrapIfAiBinding("service", binding); - - expect(result).toBe(binding); - }); - - it("does not expose non-allowlisted methods from the raw binding", ({ - expect, - }) => { - const binding = { - aiSearch: vi.fn(), - fetch: vi.fn(), - someInternalMethod: vi.fn(), - }; - - const wrapped = wrapIfAiBinding("ai", binding) as Record; - - expect(wrapped).toHaveProperty("aiSearch"); - expect(wrapped).not.toHaveProperty("fetch"); - expect(wrapped).not.toHaveProperty("someInternalMethod"); - }); - - it("propagates errors thrown by the underlying aiSearch method", async ({ - expect, - }) => { - const binding = { - aiSearch: vi.fn().mockRejectedValue(new Error("AI Search failed")), - }; - - const wrapped = wrapIfAiBinding("ai", binding) as Record< - string, - (...args: unknown[]) => Promise - >; - - await expect(wrapped.aiSearch({})).rejects.toThrow("AI Search failed"); - }); - - it("propagates RpcTarget-like return values for multi-level RPC", async ({ - expect, - }) => { - class MockAccountService { - async list() { - return [{ id: "instance-1" }]; - } - get(name: string) { - return new MockInstanceService(name); - } - } - class MockInstanceService { - constructor(public instanceId: string) {} - async search(params: { query: string }) { - return { chunks: [], search_query: params.query }; - } - } - - const binding = { - aiSearch: vi.fn().mockReturnValue(new MockAccountService()), - }; - - const wrapped = wrapIfAiBinding("ai", binding) as Record< - string, - (...args: unknown[]) => unknown - >; - - const svc = wrapped.aiSearch() as MockAccountService; - expect(await svc.list()).toEqual([{ id: "instance-1" }]); - - const inst = svc.get("my-instance"); - expect(await inst.search({ query: "test" })).toEqual({ - chunks: [], - search_query: "test", - }); - }); - - it("returns binding as-is when type is ai but no RPC methods exist", ({ - expect, - }) => { - const binding = { fetch: vi.fn() }; - - const result = wrapIfAiBinding("ai", binding); - - expect(result).toBe(binding); - }); -}); diff --git a/packages/wrangler/src/__tests__/type-generation.test.ts b/packages/wrangler/src/__tests__/type-generation.test.ts index 85191465a5f5..08ffe7d8d444 100644 --- a/packages/wrangler/src/__tests__/type-generation.test.ts +++ b/packages/wrangler/src/__tests__/type-generation.test.ts @@ -1660,6 +1660,257 @@ describe("generate types", () => { `); }); + it("should generate types from config `secrets.required`", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + secrets: { required: ["API_KEY", "DB_PASSWORD"] }, + }), + "utf-8" + ); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface Env { + API_KEY: string; + DB_PASSWORD: string; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + + it("should ignore .dev.vars when config `secrets` is defined", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + secrets: { required: ["API_KEY"] }, + }), + "utf-8" + ); + + fs.writeFileSync( + ".dev.vars", + 'DEV_VARS_SECRET="should not appear"\n', + "utf8" + ); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface Env { + API_KEY: string; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + + it("should ignore .env when config `secrets` is defined", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + secrets: { required: ["API_KEY"] }, + }), + "utf-8" + ); + + fs.writeFileSync(".env", 'DOT_ENV_SECRET="should not appear"\n', "utf8"); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface Env { + API_KEY: string; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + + it("should generate types for per-env config `secrets`", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + secrets: { required: ["API_KEY"] }, + env: { + staging: { + secrets: { + required: ["API_KEY", "DEBUG_TOKEN"], + }, + }, + production: { + secrets: { + required: ["API_KEY", "PROD_DB_PASSWORD"], + }, + }, + }, + }), + "utf-8" + ); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface StagingEnv { + API_KEY: string; + DEBUG_TOKEN: string; + } + interface ProductionEnv { + API_KEY: string; + PROD_DB_PASSWORD: string; + } + interface Env { + API_KEY: string; + DEBUG_TOKEN?: string; + PROD_DB_PASSWORD?: string; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + + it("should not fall back to .dev.vars for envs without `secrets` when another env declares `secrets`", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + env: { + staging: { + secrets: { required: ["STAGING_KEY"] }, + }, + production: {}, + }, + }), + "utf-8" + ); + + fs.writeFileSync( + ".dev.vars", + 'FALLBACK_SECRET="should not appear"\n', + "utf8" + ); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface StagingEnv { + STAGING_KEY: string; + } + interface ProductionEnv {} + interface Env { + STAGING_KEY?: string; + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + + it("should exclude .dev.vars keys when `secrets` is declared without `required`", async ({ + expect, + }) => { + fs.writeFileSync( + "./wrangler.jsonc", + JSON.stringify({ + secrets: {}, + }), + "utf-8" + ); + + fs.writeFileSync(".dev.vars", 'SHOULD_NOT_APPEAR="hidden"\n', "utf8"); + + await runWrangler("types --include-runtime=false"); + + expect(std.out).toMatchInlineSnapshot(` + " + ⛅️ wrangler x.x.x + ────────────────── + Generating project types... + + declare namespace Cloudflare { + interface Env { + } + } + interface Env extends Cloudflare.Env {} + + ──────────────────────────────────────────────────────────── + ✨ Types written to worker-configuration.d.ts + + 📣 Remember to rerun 'wrangler types' after you change your wrangler.jsonc file. + " + `); + }); + it("various different types of vars", async ({ expect }) => { fs.writeFileSync( "./wrangler.jsonc", @@ -2328,8 +2579,8 @@ describe("generate types", () => { MY_SECRET: string; } interface Env { - MY_SECRET: string; KV_STAGING?: KVNamespace; + MY_SECRET: string; } } interface Env extends Cloudflare.Env {} diff --git a/packages/wrangler/src/type-generation/index.ts b/packages/wrangler/src/type-generation/index.ts index e8611c779309..cddbff968adf 100644 --- a/packages/wrangler/src/type-generation/index.ts +++ b/packages/wrangler/src/type-generation/index.ts @@ -32,7 +32,11 @@ import { fetchPipelineTypes } from "./pipeline-schema"; import { generateRuntimeTypes } from "./runtime"; import { logRuntimeTypesMessage } from "./runtime/log-runtime-types-message"; import type { Entry } from "../deployment-bundle/entry"; -import type { Config, RawEnvironment } from "@cloudflare/workers-utils"; +import type { + Config, + RawConfig, + RawEnvironment, +} from "@cloudflare/workers-utils"; export const typesCommand = createCommand({ metadata: { @@ -330,6 +334,20 @@ export function generateImportSpecifier(from: string, to: string) { } } +/** + * Checks whether any config level (top-level or any named environment) declares + * `secrets`. Used to determine if the project has opted into config-based + * secret declarations, which replaces `.dev.vars`/`.env` inference for type generation. + */ +function hasConfigSecrets(rawConfig: RawConfig): boolean { + if (rawConfig.secrets !== undefined) { + return true; + } + return Object.values(rawConfig.env ?? {}).some( + (env) => env.secrets !== undefined + ); +} + /** * Generates TypeScript environment type definitions from a Wrangler configuration. * @@ -356,28 +374,59 @@ export async function generateEnvTypes( serviceEntries?: Map, log = true ): Promise<{ envHeader?: string; envTypes?: string }> { - // Get secret vars from .dev.vars/.env files for type generation. - // We pass an empty vars object because we only want the secret keys, - // not merged with config vars. - const secretBindings = getVarsForDev( - config.userConfigPath, - args.envFile, - {}, - args.env, - true - ); - // Extract just the keys as a Record for compatibility - // (type generation only needs the names, not the values) - const secrets: Record = {}; - for (const key of Object.keys(secretBindings)) { - secrets[key] = ""; - } - const collectionArgs = { ...args, config: config.configPath, } satisfies Partial<(typeof typesCommand)["args"]>; + const { rawConfig } = experimental_readRawConfig(collectionArgs); + + // Determine secrets source: if any config level declares `secrets`, + // the project is opted into config-based secrets (replaces .dev.vars inference). + let secrets: Record = {}; + let perEnvSecrets: Map> | undefined; + const useConfigSecrets = hasConfigSecrets(rawConfig); + + if (useConfigSecrets) { + // Config-based: build per-env secrets maps + perEnvSecrets = new Map(); + + // Top-level secrets + const topLevelKeys: Record = {}; + for (const key of rawConfig.secrets?.required ?? []) { + topLevelKeys[key] = ""; + } + perEnvSecrets.set(TOP_LEVEL_ENV_NAME, topLevelKeys); + + // Per named env secrets + for (const [envName, envConfig] of Object.entries(rawConfig.env ?? {})) { + const envKeys: Record = {}; + for (const key of envConfig.secrets?.required ?? []) { + envKeys[key] = ""; + } + perEnvSecrets.set(envName, envKeys); + } + + // For the simple path: use the specific env's secrets (or top-level) + secrets = perEnvSecrets.get(args.env ?? TOP_LEVEL_ENV_NAME) ?? {}; + } else { + // Fall back to .dev.vars/.env inference. + // We pass an empty vars object because we only want the secret keys, + // not merged with config vars. + const secretBindings = getVarsForDev( + config.userConfigPath, + args.envFile, + {}, + args.env, + true + ); + // Extract just the keys as a Record for compatibility + // (type generation only needs the names, not the values) + for (const key of Object.keys(secretBindings)) { + secrets[key] = ""; + } + } + const entrypointFormat = entrypoint?.format ?? "modules"; // Note: we infer whether the user has provided an envInterface by checking @@ -394,7 +443,6 @@ export async function generateEnvTypes( ); } - const { rawConfig } = experimental_readRawConfig(collectionArgs); const hasEnvironments = !!rawConfig.env && Object.keys(rawConfig.env).length > 0; @@ -408,6 +456,7 @@ export async function generateEnvTypes( entrypoint, serviceEntries, secrets, + perEnvSecrets, log ); } @@ -743,7 +792,8 @@ async function generateSimpleEnvTypes( * @param outputPath - The file path where the generated types will be written * @param entrypoint - Optional entry point information for the Worker * @param serviceEntries - Optional map of service names to their entry points for cross-worker type generation - * @param secrets - Record of secret variable names to their values + * @param secrets - Record of secret variable names (fallback for all envs when perEnvSecrets is not provided) + * @param perEnvSecrets - Optional per-environment secrets map. When provided, each env uses its own secrets instead of the shared fallback. * @param log - Whether to log output to the console (default: true) * * @returns An object containing the generated header comment and type definitions, or undefined values if no types were generated @@ -756,6 +806,7 @@ async function generatePerEnvironmentTypes( entrypoint?: Entry, serviceEntries?: Map, secrets: Record = {}, + perEnvSecrets?: Map>, log = true ): Promise<{ envHeader?: string; envTypes?: string }> { const { rawConfig } = experimental_readRawConfig(collectionArgs); @@ -898,6 +949,7 @@ async function generatePerEnvironmentTypes( for (const envName of envNames) { const interfaceName = toEnvInterfaceName(envName); const envBindings = new Array<{ key: string; value: string }>(); + const envSecrets = perEnvSecrets?.get(envName) ?? secrets; const bindings = bindingsPerEnv.get(envName) ?? []; for (const binding of bindings) { @@ -910,7 +962,7 @@ async function generatePerEnvironmentTypes( const vars = varsPerEnv.get(envName) ?? {}; for (const [varName, varValues] of Object.entries(vars)) { - if (varName in secrets) { + if (varName in envSecrets) { continue; } @@ -923,8 +975,9 @@ async function generatePerEnvironmentTypes( } } - for (const secretName in secrets) { + for (const secretName in envSecrets) { envBindings.push({ key: constructTypeKey(secretName), value: "string" }); + trackBinding(secretName, "string", envName); if (!stringKeys.includes(secretName)) { stringKeys.push(secretName); } @@ -1002,9 +1055,11 @@ async function generatePerEnvironmentTypes( trackBinding(binding.name, binding.type, TOP_LEVEL_ENV_NAME); } + const topLevelSecrets = perEnvSecrets?.get(TOP_LEVEL_ENV_NAME) ?? secrets; + const topLevelVars = varsPerEnv.get(TOP_LEVEL_ENV_NAME) ?? {}; for (const [varName, varValues] of Object.entries(topLevelVars)) { - if (varName in secrets) { + if (varName in topLevelSecrets) { continue; } @@ -1016,6 +1071,13 @@ async function generatePerEnvironmentTypes( } } + for (const secretName in topLevelSecrets) { + trackBinding(secretName, "string", TOP_LEVEL_ENV_NAME); + if (!stringKeys.includes(secretName)) { + stringKeys.push(secretName); + } + } + const topLevelDOs = durableObjectsPerEnv.get(TOP_LEVEL_ENV_NAME) ?? []; for (const durableObject of topLevelDOs) { const type = getDurableObjectType(durableObject); @@ -1063,19 +1125,7 @@ async function generatePerEnvironmentTypes( type: string; }>(); - for (const secretName in secrets) { - aggregatedEnvBindings.push({ - key: constructTypeKey(secretName), - required: true, - type: "string", - }); - } - for (const [name, types] of aggregatedBindings.entries()) { - if (name in secrets) { - continue; - } - const typeArray = Array.from(types); const unionType = typeArray.length === 1 ? typeArray[0] : typeArray.join(" | "); diff --git a/packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts b/packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts index ac8bfd72c048..f1f22dac955d 100644 --- a/packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts +++ b/packages/wrangler/templates/remoteBindings/ProxyServerWorker.ts @@ -3,18 +3,6 @@ import { EmailMessage } from "cloudflare:email"; interface Env extends Record {} -/** - * List of RPC methods exposed by the raw AI binding that need proxying - * through a plain-object wrapper. The raw AI binding (deployed with raw:true) - * has a non-standard prototype that capnweb's typeForRpc() doesn't recognise, - * causing "RPC stub points at a non-serializable type". By wrapping only the - * allowed RPC methods in a plain object we give capnweb an Object.prototype - * target it can navigate. - * - * Add new AI RPC method names here as they are introduced. - */ -const AI_RPC_METHODS = ["aiSearch"] as const; - class BindingNotFoundError extends Error { constructor(name?: string) { super(`Binding ${name ? `"${name}"` : ""} not found`); @@ -33,11 +21,6 @@ class BindingNotFoundError extends Error { * can't emulate that over an async boundary, we mock it locally and _actually_ * perform the .get() remotely at the first appropriate async point. See * packages/miniflare/src/workers/dispatch-namespace/dispatch-namespace.worker.ts - * - AI bindings (raw:true / minimal_mode) have a workerd-internal prototype - * that capnweb's typeForRpc() classifies as "unsupported", causing - * "RPC stub points at a non-serializable type". We wrap the binding in a - * plain object that delegates only the allowed RPC methods (AI_RPC_METHODS) - * so capnweb gets an Object.prototype target it can navigate. * * getExposedJSRPCBinding() and getExposedFetcher() perform the logic for figuring out * which binding is being accessed, dependending on the request. Note: Both have logic @@ -77,19 +60,6 @@ function getExposedJSRPCBinding(request: Request, env: Env) { }; } - if (url.searchParams.get("MF-Binding-Type") === "ai") { - const wrapper: Record unknown> = {}; - for (const method of AI_RPC_METHODS) { - if (typeof (targetBinding as any)[method] === "function") { - wrapper[method] = (...args: unknown[]) => - (targetBinding as any)[method](...args); - } - } - if (Object.keys(wrapper).length > 0) { - return wrapper; - } - } - if (url.searchParams.has("MF-Dispatch-Namespace-Options")) { const { name, args, options } = JSON.parse( url.searchParams.get("MF-Dispatch-Namespace-Options")! diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92b1cabbd6fe..3398e1657349 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2203,8 +2203,8 @@ importers: specifier: catalog:default version: 7.18.2 workerd: - specifier: 1.20260226.1 - version: 1.20260226.1 + specifier: 1.20260301.1 + version: 1.20260301.1 ws: specifier: catalog:default version: 8.18.0 @@ -4169,8 +4169,8 @@ importers: specifier: 2.0.0-rc.24 version: 2.0.0-rc.24 workerd: - specifier: 1.20260226.1 - version: 1.20260226.1 + specifier: 1.20260301.1 + version: 1.20260301.1 optionalDependencies: fsevents: specifier: ~2.3.2 @@ -5266,8 +5266,8 @@ packages: cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-64@1.20260226.1': - resolution: {integrity: sha512-Yqx+GmA+c4jTg+Icx/xC4ietdBEgqBoLTMWDIPCYmvK8uG4okdNQOVK19H1GmlD8Cm+xHSxhOJA97nY2f6x8GA==} + '@cloudflare/workerd-darwin-64@1.20260301.1': + resolution: {integrity: sha512-+kJvwociLrvy1JV9BAvoSVsMEIYD982CpFmo/yMEvBwxDIjltYsLTE8DLi0mCkGsQ8Ygidv2fD9wavzXeiY7OQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] @@ -5284,8 +5284,8 @@ packages: cpu: [arm64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20260226.1': - resolution: {integrity: sha512-wnkdfztvQZB9mppmQuXZtvI+As9E9LZCLy6cNXHqX7M7aDB3TjmMJ7Ses5hVWFRPF/v+J/DHIgXLUNtcz+JW6g==} + '@cloudflare/workerd-darwin-arm64@1.20260301.1': + resolution: {integrity: sha512-PPIetY3e67YBr9O4UhILK8nbm5TqUDl14qx4rwFNrRSBOvlzuczzbd4BqgpAtbGVFxKp1PWpjAnBvGU/OI/tLQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] @@ -5302,8 +5302,8 @@ packages: cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-64@1.20260226.1': - resolution: {integrity: sha512-2IXlV/4/1ofnIjG48DsyHV8R82EJBCnTwsgDZbMWgLbrkdlBAl7hCCbtVfOCJs7/iQ6l2PtjMO2PSNS7qZlTaA==} + '@cloudflare/workerd-linux-64@1.20260301.1': + resolution: {integrity: sha512-Gu5vaVTZuYl3cHa+u5CDzSVDBvSkfNyuAHi6Mdfut7TTUdcb3V5CIcR/mXRSyMXzEy9YxEWIfdKMxOMBjupvYQ==} engines: {node: '>=16'} cpu: [x64] os: [linux] @@ -5320,8 +5320,8 @@ packages: cpu: [arm64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20260226.1': - resolution: {integrity: sha512-kV8CWLdC9JoxjJu0ap63Fi38xANfrXCFu5ldkc6DOW/VyTSaFShVkNH12X4P6aB5VXR1JHFc5jnZvoEImrfArA==} + '@cloudflare/workerd-linux-arm64@1.20260301.1': + resolution: {integrity: sha512-igL1pkyCXW6GiGpjdOAvqMi87UW0LMc/+yIQe/CSzuZJm5GzXoAMrwVTkCFnikk6JVGELrM5x0tGYlxa0sk5Iw==} engines: {node: '>=16'} cpu: [arm64] os: [linux] @@ -5338,8 +5338,8 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workerd-windows-64@1.20260226.1': - resolution: {integrity: sha512-bOCPxafwcEi+ShDNQNbCQqiSsg2F/rykeX5+0SIY80DIacPLXztnd02+hqGbCd+sb5DMnW9ovvrCNxOr0aJJMw==} + '@cloudflare/workerd-windows-64@1.20260301.1': + resolution: {integrity: sha512-Q0wMJ4kcujXILwQKQFc1jaYamVsNvjuECzvRrTI8OxGFMx2yq9aOsswViE4X1gaS2YQQ5u0JGwuGi5WdT1Lt7A==} engines: {node: '>=16'} cpu: [x64] os: [win32] @@ -14901,8 +14901,8 @@ packages: engines: {node: '>=16'} hasBin: true - workerd@1.20260226.1: - resolution: {integrity: sha512-veMyr994Cq0ExD0QCRH4JL3fZAs4mMjdodWA9Sfzk1aUyKI2VNOPixyVg0GJmJJGzbapbvKHLRgMq9obGImotw==} + workerd@1.20260301.1: + resolution: {integrity: sha512-oterQ1IFd3h7PjCfT4znSFOkJCvNQ6YMOyZ40YsnO3nrSpgB4TbJVYWFOnyJAw71/RQuupfVqZZWKvsy8GO3fw==} engines: {node: '>=16'} hasBin: true @@ -16437,7 +16437,7 @@ snapshots: devalue: 5.3.2 miniflare: 4.20251210.0 semver: 7.7.3 - vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.0(@types/node@20.19.9)(typescript@5.8.3))(supports-color@9.2.2)(tsx@4.21.0)(yaml@2.8.1) + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(msw@2.12.0(@types/node@20.19.9)(typescript@5.9.3))(tsx@4.21.0)(yaml@2.8.1) wrangler: 4.54.0(@cloudflare/workers-types@4.20260305.0) zod: 3.25.76 transitivePeerDependencies: @@ -16451,7 +16451,7 @@ snapshots: '@cloudflare/workerd-darwin-64@1.20260218.0': optional: true - '@cloudflare/workerd-darwin-64@1.20260226.1': + '@cloudflare/workerd-darwin-64@1.20260301.1': optional: true '@cloudflare/workerd-darwin-arm64@1.20251210.0': @@ -16460,7 +16460,7 @@ snapshots: '@cloudflare/workerd-darwin-arm64@1.20260218.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20260226.1': + '@cloudflare/workerd-darwin-arm64@1.20260301.1': optional: true '@cloudflare/workerd-linux-64@1.20251210.0': @@ -16469,7 +16469,7 @@ snapshots: '@cloudflare/workerd-linux-64@1.20260218.0': optional: true - '@cloudflare/workerd-linux-64@1.20260226.1': + '@cloudflare/workerd-linux-64@1.20260301.1': optional: true '@cloudflare/workerd-linux-arm64@1.20251210.0': @@ -16478,7 +16478,7 @@ snapshots: '@cloudflare/workerd-linux-arm64@1.20260218.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20260226.1': + '@cloudflare/workerd-linux-arm64@1.20260301.1': optional: true '@cloudflare/workerd-windows-64@1.20251210.0': @@ -16487,7 +16487,7 @@ snapshots: '@cloudflare/workerd-windows-64@1.20260218.0': optional: true - '@cloudflare/workerd-windows-64@1.20260226.1': + '@cloudflare/workerd-windows-64@1.20260301.1': optional: true '@cloudflare/workers-editor-shared@0.1.1(@cloudflare/style-const@5.7.3(react@19.2.1))(@cloudflare/style-container@7.12.2(@cloudflare/style-const@5.7.3(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': @@ -26942,13 +26942,13 @@ snapshots: '@cloudflare/workerd-linux-arm64': 1.20260218.0 '@cloudflare/workerd-windows-64': 1.20260218.0 - workerd@1.20260226.1: + workerd@1.20260301.1: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260226.1 - '@cloudflare/workerd-darwin-arm64': 1.20260226.1 - '@cloudflare/workerd-linux-64': 1.20260226.1 - '@cloudflare/workerd-linux-arm64': 1.20260226.1 - '@cloudflare/workerd-windows-64': 1.20260226.1 + '@cloudflare/workerd-darwin-64': 1.20260301.1 + '@cloudflare/workerd-darwin-arm64': 1.20260301.1 + '@cloudflare/workerd-linux-64': 1.20260301.1 + '@cloudflare/workerd-linux-arm64': 1.20260301.1 + '@cloudflare/workerd-windows-64': 1.20260301.1 wrangler@4.54.0(@cloudflare/workers-types@4.20260305.0): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d7321b3025f2..7795d5cad087 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,7 @@ catalog: esbuild: "0.27.3" playwright-chromium: "^1.56.1" "@cloudflare/workers-types": "^4.20260226.1" - workerd: "1.20260226.1" + workerd: "1.20260301.1" eslint: "^9.39.1" jsonc-parser: "^3.2.0" smol-toml: "^1.5.2"