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
27 changes: 27 additions & 0 deletions .changeset/new-config-settings-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@cloudflare/config": minor
"@cloudflare/vite-plugin": minor
"wrangler": minor
---

Add a `settings` export to the experimental `cloudflare.config.ts` config

Account-level settings (`accountId`, `complianceRegion`) now live in a dedicated, named `settings` export authored via `defineSettings`, rather than on the Worker config. A `cloudflare.config.ts` can export at most one `settings` object; the Worker itself is the `default` export.

```jsonc
// cloudflare.config.ts
import { defineSettings, defineWorker } from "wrangler/experimental-config";
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

export const settings = defineSettings({
accountId: "<your-account-id>",
});

export default defineWorker({
name: "my-worker",
entrypoint,
compatibilityDate: "2026-05-18",
});
```

This is only used behind the experimental new-config path (`wrangler --experimental-new-config` and the `@cloudflare/vite-plugin` `experimental.newConfig` option).
10 changes: 9 additions & 1 deletion fixtures/experimental-new-config/cloudflare.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { bindings, defineWorker } from "wrangler/experimental-config";
import {
bindings,
defineSettings,
defineWorker,
} from "wrangler/experimental-config";
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };

export const settings = defineSettings({
complianceRegion: "public",
});

export default defineWorker((ctx) => ({
name: "experimental-new-config",
entrypoint,
Expand Down
69 changes: 48 additions & 21 deletions packages/config/src/__tests__/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@ import { bindings } from "../bindings";
import { convertToWranglerConfig } from "../convert";
import { exports as exportConfig } from "../exports";

const baseConfig = { name: "worker", compatibilityDate: "2026-06-01" } as const;
const baseConfig = {
type: "worker",
name: "my-worker",
compatibilityDate: "2026-06-01",
} as const;

describe("convertToWranglerConfig", () => {
describe("top-level fields", () => {
it("maps primitive top-level fields", ({ expect }) => {
const result = convertToWranglerConfig({
type: "worker",
name: "my-worker",
entrypoint: "./src/index.ts",
accountId: "acc-123",
compatibilityDate: "2026-01-01",
compatibilityFlags: ["nodejs_compat"],
workersDev: true,
Expand All @@ -22,7 +26,6 @@ describe("convertToWranglerConfig", () => {
expect(result).toEqual({
name: "my-worker",
main: "./src/index.ts",
account_id: "acc-123",
compatibility_date: "2026-01-01",
compatibility_flags: ["nodejs_compat"],
workers_dev: true,
Expand All @@ -32,24 +35,6 @@ describe("convertToWranglerConfig", () => {
});
});

it("maps complianceRegion: 'fedramp-high' to 'fedramp_high'", ({
expect,
}) => {
const result = convertToWranglerConfig({
...baseConfig,
complianceRegion: "fedramp-high",
});
expect(result.compliance_region).toBe("fedramp_high");
});

it("passes complianceRegion: 'public' through unchanged", ({ expect }) => {
const result = convertToWranglerConfig({
...baseConfig,
complianceRegion: "public",
});
expect(result.compliance_region).toBe("public");
});

it("passes placement through unchanged", ({ expect }) => {
const result = convertToWranglerConfig({
...baseConfig,
Expand Down Expand Up @@ -1072,4 +1057,46 @@ describe("convertToWranglerConfig", () => {
expect(result.streaming_tail_consumers).toEqual([{ service: "b" }]);
});
});

describe("settings", () => {
it("maps accountId to account_id", ({ expect }) => {
const result = convertToWranglerConfig(baseConfig, {
type: "settings",
accountId: "acc-123",
});
expect(result.account_id).toBe("acc-123");
});

it("maps complianceRegion: 'fedramp-high' to 'fedramp_high'", ({
expect,
}) => {
const result = convertToWranglerConfig(baseConfig, {
type: "settings",
complianceRegion: "fedramp-high",
});
expect(result.compliance_region).toBe("fedramp_high");
});

it("passes complianceRegion: 'public' through unchanged", ({ expect }) => {
const result = convertToWranglerConfig(baseConfig, {
type: "settings",
complianceRegion: "public",
});
expect(result.compliance_region).toBe("public");
});

it("sets no settings fields when none are provided", ({ expect }) => {
const result = convertToWranglerConfig(baseConfig, { type: "settings" });
expect(result.account_id).toBeUndefined();
expect(result.compliance_region).toBeUndefined();
});

it("sets no settings fields when settings config is omitted", ({
expect,
}) => {
const result = convertToWranglerConfig(baseConfig);
expect(result.account_id).toBeUndefined();
expect(result.compliance_region).toBeUndefined();
});
});
});
58 changes: 52 additions & 6 deletions packages/config/src/__tests__/load.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,28 @@ import { InputWorkerSchema } from "../schema";
// inside a test directly. Instead, we run a small Node program in a
// subprocess that calls `loadConfig`, serialises the result as JSON, and
// prints it to stdout for the test to consume.
function runLoadConfigInSubprocess(args: { cwd: string; configPath: string }): {
function runLoadConfigInSubprocess(args: {
cwd: string;
configPath: string;
include?: string[];
}): {
config: unknown;
exports: Record<string, unknown>;
dependencies: string[];
} {
// Use a file:// URL rather than a raw filesystem path so the embedded
// `import` specifier is valid on Windows (where absolute paths like
// `C:\...` are not accepted as ESM specifiers).
const sourceEntry = pathToFileURL(path.resolve(__dirname, "../load.ts")).href;
const options = args.include
? `, { include: ${JSON.stringify(args.include)} }`
: "";
const script = `
import { loadConfig } from ${JSON.stringify(sourceEntry)};
const result = await loadConfig(${JSON.stringify(args.configPath)});
const result = await loadConfig(${JSON.stringify(args.configPath)}${options});
const serialisable = {
config: result.config,
config: result.exports.default,
exports: result.exports,
dependencies: [...result.dependencies],
};
process.stdout.write(JSON.stringify(serialisable, (_, v) => {
Expand Down Expand Up @@ -65,6 +74,43 @@ describe("loadConfig", () => {
expect(result.config).toEqual({ name: "my-worker" });
});

it("returns all named exports keyed by name", async ({ expect }) => {
await seed({
"cloudflare.config.ts": `
export default { type: "worker", name: "w" };
export const settings = { type: "settings", accountId: "acc-123" };
`,
});

const result = runLoadConfigInSubprocess({
cwd: process.cwd(),
configPath: "./cloudflare.config.ts",
});

expect(result.exports.default).toEqual({ type: "worker", name: "w" });
expect(result.exports.settings).toEqual({
type: "settings",
accountId: "acc-123",
});
});

it("filters exports by `include` before resolution", async ({ expect }) => {
await seed({
"cloudflare.config.ts": `
export default { type: "worker", name: "w" };
export const settings = { type: "settings" };
`,
});

const result = runLoadConfigInSubprocess({
cwd: process.cwd(),
configPath: "./cloudflare.config.ts",
include: ["settings"],
});

expect(Object.keys(result.exports)).toEqual(["settings"]);
});

it("anchors relative cf-worker specifiers to an absolute path without executing them", async ({
expect,
}) => {
Expand Down Expand Up @@ -152,7 +198,7 @@ describe("loadConfig", () => {
"src/index.ts": `// not executed`,
"cloudflare.config.ts": `
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };
export default { name: "worker", compatibilityDate: "2026-06-01", entrypoint };
export default { type: "worker", name: "worker", compatibilityDate: "2026-06-01", entrypoint };
`,
});

Expand Down Expand Up @@ -182,8 +228,8 @@ describe("loadConfig", () => {
writeFileSync("./cloudflare.config.ts", 'export default { name: "second" };');
const second = await loadConfig("./cloudflare.config.ts");
process.stdout.write(JSON.stringify({
first: first.config,
second: second.config,
first: first.exports.default,
second: second.exports.default,
}));
`;
const sub = spawnSync(
Expand Down
141 changes: 138 additions & 3 deletions packages/config/src/__tests__/schema.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { describe, it } from "vitest";
import { InputWorkerSchema, OutputWorkerSchema } from "../schema";

const baseConfig = { name: "worker", compatibilityDate: "2026-06-01" } as const;
import {
ConfigExportsSchema,
InputWorkerSchema,
OutputWorkerSchema,
SettingsSchema,
} from "../schema";

const baseConfig = {
type: "worker",
name: "my-worker",
compatibilityDate: "2026-06-01",
} as const;

describe("InputWorkerSchema", () => {
describe("env singleton bindings", () => {
Expand Down Expand Up @@ -589,3 +598,129 @@ describe("OutputWorkerSchema", () => {
expect(result.success).toBe(false);
});
});

describe("InputWorkerSchema type discriminant", () => {
it("requires type: 'worker'", ({ expect }) => {
const { type: _type, ...withoutType } = baseConfig;
const result = InputWorkerSchema.safeParse(withoutType);

expect(result.success).toBe(false);
});
});

describe("SettingsSchema", () => {
it("accepts a minimal settings config", ({ expect }) => {
const result = SettingsSchema.safeParse({ type: "settings" });

expect(result.success).toBe(true);
});

it("accepts accountId and complianceRegion", ({ expect }) => {
const result = SettingsSchema.safeParse({
type: "settings",
accountId: "acc-123",
complianceRegion: "fedramp-high",
});

expect(result.success).toBe(true);
});

it("rejects unknown fields", ({ expect }) => {
const result = SettingsSchema.safeParse({
type: "settings",
name: "my-worker",
});

expect(result.success).toBe(false);
});
});

describe("ConfigExportsSchema", () => {
it("discriminates worker and settings exports by type", ({ expect }) => {
const result = ConfigExportsSchema.safeParse({
default: baseConfig,
settings: { type: "settings", accountId: "acc-123" },
});

expect(result.success).toBe(true);
if (result.success) {
expect(result.data.default?.type).toBe("worker");
expect(result.data.settings?.type).toBe("settings");
}
});

it("reports an invalid-discriminator issue keyed by export name", ({
expect,
}) => {
const result = ConfigExportsSchema.safeParse({
default: { name: "my-worker", compatibilityDate: "2026-06-01" },
});

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0]?.path).toEqual(["default", "type"]);
}
});

it("rejects a settings config on a non-`settings` export", ({ expect }) => {
const result = ConfigExportsSchema.safeParse({
default: baseConfig,
settings: { type: "settings" },
extraSettings: { type: "settings" },
});

expect(result.success).toBe(false);
if (!result.success) {
const issue = result.error.issues.find((i) =>
i.message.includes(
"A `settings` config is only allowed on the `settings` export"
)
);
expect(issue?.path).toEqual(["extraSettings"]);
}
});

it("rejects a settings config on the `default` export", ({ expect }) => {
const result = ConfigExportsSchema.safeParse({
default: { type: "settings" },
});

expect(result.success).toBe(false);
if (!result.success) {
const issue = result.error.issues.find((i) =>
i.message.includes(
"A `settings` config is only allowed on the `settings` export"
)
);
expect(issue?.path).toEqual(["default"]);
}
});

it("rejects a worker config on the reserved `settings` export", ({
expect,
}) => {
const result = ConfigExportsSchema.safeParse({
default: baseConfig,
settings: { ...baseConfig, name: "settings" },
});

expect(result.success).toBe(false);
if (!result.success) {
const issue = result.error.issues.find((i) =>
i.message.includes(
"The `settings` export is reserved for a `settings` config"
)
);
expect(issue?.path).toEqual(["settings"]);
}
});

it("allows multiple worker exports", ({ expect }) => {
const result = ConfigExportsSchema.safeParse({
default: baseConfig,
api: { ...baseConfig, name: "api" },
});

expect(result.success).toBe(true);
});
});
Loading
Loading