Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .changeset/new-config-modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"wrangler": minor
---

Add `modes` to `cloudflare.config.ts` for declarative per-environment config

This extends the experimental `--x-new-config` feature. Per-environment config was previously only expressible by branching on `ctx.mode` inside the function form of a config. That works at runtime, but it is opaque to `wrangler types`: the generated `Env` collapses every branch together, so bindings that only exist in one environment cannot be distinguished from ones that always exist.

`modes` declares those differences statically instead. Each entry is a partial Worker config layered over the base when that mode is selected with `--env` or `CLOUDFLARE_ENV`. `env` and `exports` merge per key so a mode only states what differs; every other field replaces the base value outright.

```ts
export default defineWorker({
name: "my-worker",
compatibilityDate: "2026-06-01",
env: { SHARED_KV: bindings.kv({ id: "..." }) },
modes: {
staging: { env: { API_KEY: bindings.secret() } },
production: {
name: "my-worker-prod",
env: {
API_KEY: bindings.secret(),
ANALYTICS: bindings.r2({ name: "..." }),
},
},
},
});
```

Because modes are declared rather than computed, `wrangler types` can now aggregate them the same way it does named environments in the Wrangler JSON config. A binding every mode declares is required on `Env`, one that only some declare is optional, and its type is the union of the types the declaring modes give it. Each mode's exact `Env` is also available as `Cloudflare.EnvFor<"production">`, with the declared names as `Cloudflare.Mode`.

Combined with ordinary `import` statements, this lets a large config be split across files while keeping a single aggregated `Env`. Branching on `ctx.mode` continues to work unchanged for configs that do not declare `modes`.
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/* eslint-disable */
// Generated by @cloudflare/config
type __WorkerConfig = import("wrangler/experimental-config").UnwrapConfig<typeof import("./cloudflare.config").default>;
type __Env = import("wrangler/experimental-config").InferEnv<__WorkerConfig>;
type __Env = import("wrangler/experimental-config").InferAggregatedEnv<__WorkerConfig>;

declare namespace Cloudflare {
interface GlobalProps {
mainModule: import("wrangler/experimental-config").InferMainModule<__WorkerConfig>;
durableNamespaces: import("wrangler/experimental-config").InferDurableNamespaces<__WorkerConfig>;
}
interface Env extends __Env {}
/** The modes declared in your config, or `never` if there are none. */
type Mode = import("wrangler/experimental-config").InferModeNames<__WorkerConfig>;
/** The `Env` for one specific mode, with that mode's overrides applied. */
type EnvFor<TMode extends Mode> = import("wrangler/experimental-config").InferEnvForMode<__WorkerConfig, TMode>;
}
interface Env extends Cloudflare.Env {}
12 changes: 11 additions & 1 deletion packages/config/src/__tests__/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,21 @@ describe("generateTypes", () => {
}) => {
const out = generateTypes({ configPath: "./cloudflare.config.ts" });
expect(out).toContain(
`type __Env = import("@cloudflare/config").InferEnv<__WorkerConfig>;`
`type __Env = import("@cloudflare/config").InferAggregatedEnv<__WorkerConfig>;`
);
expect(out).toContain(`interface Env extends __Env {}`);
});

it("exposes the declared modes and their per-mode envs", ({ expect }) => {
const out = generateTypes({ configPath: "./cloudflare.config.ts" });
expect(out).toContain(
`type Mode = import("@cloudflare/config").InferModeNames<__WorkerConfig>;`
);
expect(out).toContain(
`type EnvFor<TMode extends Mode> = import("@cloudflare/config").InferEnvForMode<__WorkerConfig, TMode>;`
);
});

it("emits a global script (no top-level import/export, no declare global)", ({
expect,
}) => {
Expand Down
Loading