Skip to content
Closed
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
15 changes: 15 additions & 0 deletions .changeset/slow-dolphins-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"wrangler": patch
---

feat: support extending the user configuration

The main goal here is to enable tools to generate a partial configuration file that is merged into the user configuration when Wrangler commands are run.

The file must be written to `./.wrangler/config/extra.json`, where the path is relative to the project path, which is the directory containing the wrangler.toml or the current working directory if there is no wrangler.toml.

The format of the file is a JSON object whose properties are the inheritable and non-inheritable options described in the Wrangler configuration documentation. Notably it cannot contain the "top level" configuration properties.

The contents of the file will be merged into the configuration of the currently selected environment before being used in all Wrangler commands.

The user does not need to manually specify that this merging should happen. It is done automatically when the file is found.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"cloudflareaccess",
"cloudflared",
"Codespaces",
"defu",
"esbuild",
"eslintcache",
"execa",
Expand Down
3 changes: 2 additions & 1 deletion packages/wrangler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"generate-json-schema": "pnpm exec ts-json-schema-generator --no-type-check --path src/config/config.ts --type RawConfig --out config-schema.json",
"prepublishOnly": "SOURCEMAPS=false pnpm run -w build",
"start": "pnpm run bundle && cross-env NODE_OPTIONS=--enable-source-maps ./bin/wrangler.js",
"test": "pnpm run assert-git-version && vitest",
"test": "pnpm run assert-git-version && cross-env LC_ALL=C vitest",
"test:ci": "pnpm run test run",
"test:debug": "pnpm run test --silent=false --verbose=true",
"test:e2e": "vitest -c ./e2e/vitest.config.mts",
Expand Down Expand Up @@ -125,6 +125,7 @@
"cmd-shim": "^4.1.0",
"command-exists": "^1.2.9",
"concurrently": "^8.2.2",
"defu": "^6.1.4",
"devtools-protocol": "^0.0.1182435",
"dotenv": "^16.0.0",
"execa": "^6.1.0",
Expand Down
232 changes: 231 additions & 1 deletion packages/wrangler/src/__tests__/configuration.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import path from "node:path";
import { readConfig } from "../config";
import { normalizeAndValidateConfig } from "../config/validation";
import { mockConsoleMethods } from "./helpers/mock-console";
import { normalizeString } from "./helpers/normalize";
import { runInTempDir } from "./helpers/run-in-tmp";
import { writeWranglerToml } from "./helpers/write-wrangler-toml";
import {
writeExtraJson,
writeWranglerToml,
} from "./helpers/write-wrangler-toml";
import type {
ConfigFields,
RawConfig,
Expand All @@ -12,6 +16,7 @@ import type {
} from "../config";

describe("readConfig()", () => {
const std = mockConsoleMethods();
runInTempDir();
it("should not error if a python entrypoint is used with the right compatibility_flag", () => {
writeWranglerToml({
Expand Down Expand Up @@ -43,6 +48,231 @@ describe("readConfig()", () => {
);
}
});

describe("extended configuration", () => {
it("should extend the user config with config from .wrangler/config/extra.json", () => {
const main = "src/index.ts";
const resolvedMain = path.resolve(process.cwd(), main);

writeWranglerToml({
main,
});
writeExtraJson({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
});

const config = readConfig("wrangler.toml", {});
expect(config).toEqual(
expect.objectContaining({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
configPath: "wrangler.toml",
main: resolvedMain,
})
);

expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"info": "Loading additional configuration from .wrangler/config/extra.json.",
"out": "",
"warn": "",
}
`);
});

it("should overwrite config with matching properties from .wrangler/config/extra.json", () => {
writeWranglerToml({
main: "src/index.js",
compatibility_date: "2021-01-01",
});

// Note that paths are relative ot the extra.json file.
const main = "../../dist/index.ts";
const resolvedMain = path.resolve(
process.cwd(),
".wrangler/config",
main
);

writeExtraJson({
main,
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
});

const config = readConfig("wrangler.toml", {});
expect(config).toEqual(
expect.objectContaining({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
main: resolvedMain,
})
);
});

it("should concatenate array-based config with matching properties from .wrangler/config/extra.json", () => {
writeWranglerToml({
main: "src/index.js",
compatibility_flags: ["allow_custom_ports"],
});

writeExtraJson({
compatibility_flags: ["nodejs_compat"],
});

const config = readConfig("wrangler.toml", {});
expect(config).toEqual(
expect.objectContaining({
compatibility_flags: ["nodejs_compat", "allow_custom_ports"],
})
);
});

it("should merge object-based config with matching properties from .wrangler/config/extra.json", () => {
writeWranglerToml({
main: "src/index.js",
assets: {
directory: "./public",
not_found_handling: "404-page",
},
});

writeExtraJson({
assets: {
// Note that Environment validation and typings require that directory exists,
// so the extra.json would always have to provide this property
// even if it just wanted to augment the rest of the object with extra properties.
directory: "./public",
binding: "ASSETS",
},
});

const config = readConfig("wrangler.toml", {});
expect(config).toEqual(
expect.objectContaining({
assets: {
binding: "ASSETS",
directory: "./public",
not_found_handling: "404-page",
},
})
);
});

it("should error and warn if the extra config is not valid Environment config", () => {
writeWranglerToml({});
writeExtraJson({
compatibility_date: 2021,
unexpected_property: true,
} as unknown as RawEnvironment);

let error = new Error("Missing expected error");
try {
readConfig("wrangler.toml", {});
} catch (e) {
error = e as Error;
}

expect(error.toString().replaceAll("\\", "/")).toMatchInlineSnapshot(`
"Error: Processing wrangler.toml configuration:

- Processing extra configuration found in .wrangler/config/extra.json.
- Expected \\"compatibility_date\\" to be of type string but got 2021."
`);

expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"info": "Loading additional configuration from .wrangler/config/extra.json.",
"out": "",
"warn": "▲ [WARNING] Processing wrangler.toml configuration:


- Processing extra configuration found in .wrangler/config/extra.json.
- Unexpected fields found in extended config field: \\"unexpected_property\\"

",
}
`);
});

it("should override the selected named environment", () => {
const main = "src/index.ts";
const resolvedMain = path.resolve(process.cwd(), main);

writeWranglerToml({
main,
env: {
prod: {
compatibility_date: "2021-01-01",
logpush: true,
},
dev: {
compatibility_date: "2022-02-02",
no_bundle: true,
},
},
});
writeExtraJson({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
no_bundle: true,
});

const prodConfig = readConfig("wrangler.toml", { env: "prod" });
expect(prodConfig).toEqual(
expect.objectContaining({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
configPath: "wrangler.toml",
main: resolvedMain,
logpush: true,
no_bundle: true,
})
);

const devConfig = readConfig("wrangler.toml", { env: "dev" });
expect(devConfig).toEqual(
expect.objectContaining({
compatibility_date: "2024-11-01",
compatibility_flags: ["nodejs_compat"],
configPath: "wrangler.toml",
main: resolvedMain,
no_bundle: true,
})
);
});

it("should support extending with a Pages output directory property", () => {
const pages_build_output_dir = "./public";

writeWranglerToml({});
writeExtraJson({
pages_build_output_dir,
});

const config = readConfig("wrangler.toml", {});
expect(config).toEqual(
expect.objectContaining({
pages_build_output_dir: "./public",
})
);

expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"info": "Loading additional configuration from .wrangler/config/extra.json.",
"out": "",
"warn": "",
}
`);
});
});
});

describe("normalizeAndValidateConfig()", () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/wrangler/src/__tests__/helpers/write-wrangler-toml.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as fs from "fs";
import TOML from "@iarna/toml";
import type { RawConfig } from "../../config";
import { PagesConfigFields } from "../../config/config";

Check failure on line 3 in packages/wrangler/src/__tests__/helpers/write-wrangler-toml.ts

View workflow job for this annotation

GitHub Actions / Checks

All imports in the declaration are only used as types. Use `import type`
import { ensureDirectoryExistsSync } from "../../utils/filesystem";
import type { RawConfig, RawEnvironment } from "../../config";

/** Write a mock wrangler.toml file to disk. */
export function writeWranglerToml(
Expand Down Expand Up @@ -33,3 +35,11 @@
"utf-8"
);
}

export function writeExtraJson(
config: RawEnvironment & Partial<PagesConfigFields> = {},
path = "./.wrangler/config/extra.json"
) {
ensureDirectoryExistsSync(path);
fs.writeFileSync(path, JSON.stringify(config), "utf-8");
}
7 changes: 5 additions & 2 deletions packages/wrangler/src/__tests__/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/consistent-type-imports */
import assert from "node:assert";
import { resolve } from "path";
import { PassThrough } from "stream";
import chalk from "chalk";
Expand All @@ -22,8 +23,10 @@ chalk.level = 0;
global as unknown as { __RELATIVE_PACKAGE_PATH__: string }
).__RELATIVE_PACKAGE_PATH__ = "..";

// Set `LC_ALL` to fix the language as English for the messages thrown by Yargs.
process.env.LC_ALL = "en";
assert(
process.env.LC_ALL === "C",
"Expected `LC_ALL` env var to be 'C' in order get deterministic localized messages in tests"
);

vi.mock("ansi-escapes", () => {
return {
Expand Down
14 changes: 1 addition & 13 deletions packages/wrangler/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,6 @@ export type RawConfig = Partial<ConfigFields<RawDevConfig>> &
DeprecatedConfigFields &
EnvironmentMap & { $schema?: string };

// Pages-specific configuration fields
interface PagesConfigFields {
/**
* The directory of static assets to serve.
*
* The presence of this field in `wrangler.toml` indicates a Pages project,
* and will prompt the handling of the configuration file according to the
* Pages-specific validation rules.
*/
pages_build_output_dir?: string;
}

export interface ConfigFields<Dev extends RawDevConfig> {
configPath: string | undefined;

Expand Down Expand Up @@ -185,7 +173,7 @@ export interface ConfigFields<Dev extends RawDevConfig> {
}

// Pages-specific configuration fields
interface PagesConfigFields {
export interface PagesConfigFields {
/**
* The directory of static assets to serve.
*
Expand Down
Loading
Loading