Skip to content

Commit b7415b9

Browse files
feat: support extending the user configuration
1 parent 4ee43c7 commit b7415b9

File tree

8 files changed

+304
-7
lines changed

8 files changed

+304
-7
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"cloudflareaccess",
1212
"cloudflared",
1313
"Codespaces",
14+
"defu",
1415
"esbuild",
1516
"eslintcache",
1617
"execa",

packages/wrangler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
"cmd-shim": "^4.1.0",
126126
"command-exists": "^1.2.9",
127127
"concurrently": "^8.2.2",
128+
"defu": "^6.1.4",
128129
"devtools-protocol": "^0.0.1182435",
129130
"dotenv": "^16.0.0",
130131
"execa": "^6.1.0",

packages/wrangler/src/__tests__/configuration.test.ts

Lines changed: 194 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import path from "node:path";
22
import { readConfig } from "../config";
33
import { normalizeAndValidateConfig } from "../config/validation";
4+
import { mockConsoleMethods } from "./helpers/mock-console";
45
import { normalizeString } from "./helpers/normalize";
56
import { runInTempDir } from "./helpers/run-in-tmp";
6-
import { writeWranglerToml } from "./helpers/write-wrangler-toml";
7+
import {
8+
writeExtraJson,
9+
writeWranglerToml,
10+
} from "./helpers/write-wrangler-toml";
711
import type {
812
ConfigFields,
913
RawConfig,
@@ -12,6 +16,7 @@ import type {
1216
} from "../config";
1317

1418
describe("readConfig()", () => {
19+
const std = mockConsoleMethods();
1520
runInTempDir();
1621
it("should not error if a python entrypoint is used with the right compatibility_flag", () => {
1722
writeWranglerToml({
@@ -43,6 +48,194 @@ describe("readConfig()", () => {
4348
);
4449
}
4550
});
51+
52+
describe("extended configuration", () => {
53+
it("should extend the user config with config from .wrangler/config/extra.json", () => {
54+
const main = "src/index.ts";
55+
const resolvedMain = path.resolve(process.cwd(), main);
56+
57+
writeWranglerToml({
58+
main,
59+
});
60+
writeExtraJson({
61+
compatibility_date: "2024-11-01",
62+
compatibility_flags: ["nodejs_compat"],
63+
});
64+
65+
const config = readConfig("wrangler.toml", {});
66+
expect(config).toEqual(
67+
expect.objectContaining({
68+
compatibility_date: "2024-11-01",
69+
compatibility_flags: ["nodejs_compat"],
70+
configPath: "wrangler.toml",
71+
main: resolvedMain,
72+
})
73+
);
74+
75+
expect(std).toMatchInlineSnapshot(`
76+
Object {
77+
"debug": "",
78+
"err": "",
79+
"info": "Extending with configuration found in .wrangler/config/extra.json.",
80+
"out": "",
81+
"warn": "",
82+
}
83+
`);
84+
});
85+
86+
it("should overwrite config with matching properties from .wrangler/config/extra.json", () => {
87+
writeWranglerToml({
88+
main: "src/index.js",
89+
compatibility_date: "2021-01-01",
90+
});
91+
92+
// Note that paths are relative ot the extra.json file.
93+
const main = "../../dist/index.ts";
94+
const resolvedMain = path.resolve(
95+
process.cwd(),
96+
".wrangler/config",
97+
main
98+
);
99+
100+
writeExtraJson({
101+
main,
102+
compatibility_date: "2024-11-01",
103+
compatibility_flags: ["nodejs_compat"],
104+
});
105+
106+
const config = readConfig("wrangler.toml", {});
107+
expect(config).toEqual(
108+
expect.objectContaining({
109+
compatibility_date: "2024-11-01",
110+
compatibility_flags: ["nodejs_compat"],
111+
main: resolvedMain,
112+
})
113+
);
114+
});
115+
116+
it("should concatenate array-based config with matching properties from .wrangler/config/extra.json", () => {
117+
writeWranglerToml({
118+
main: "src/index.js",
119+
compatibility_flags: ["allow_custom_ports"],
120+
});
121+
122+
writeExtraJson({
123+
compatibility_flags: ["nodejs_compat"],
124+
});
125+
126+
const config = readConfig("wrangler.toml", {});
127+
expect(config).toEqual(
128+
expect.objectContaining({
129+
compatibility_flags: ["nodejs_compat", "allow_custom_ports"],
130+
})
131+
);
132+
});
133+
134+
it("should merge object-based config with matching properties from .wrangler/config/extra.json", () => {
135+
writeWranglerToml({
136+
main: "src/index.js",
137+
assets: {
138+
directory: "./public",
139+
not_found_handling: "404-page",
140+
},
141+
});
142+
143+
writeExtraJson({
144+
assets: {
145+
// Note that Environment validation and typings require that directory exists,
146+
// so the extra.json would always have to provide this property
147+
// even if it just wanted to augment the rest of the object with extra properties.
148+
directory: "./public",
149+
binding: "ASSETS",
150+
},
151+
});
152+
153+
const config = readConfig("wrangler.toml", {});
154+
expect(config).toEqual(
155+
expect.objectContaining({
156+
assets: {
157+
binding: "ASSETS",
158+
directory: "./public",
159+
not_found_handling: "404-page",
160+
},
161+
})
162+
);
163+
});
164+
165+
it("should error and warn if the extra config is not valid Environment config", () => {
166+
writeWranglerToml({});
167+
writeExtraJson({
168+
compatibility_date: 2021,
169+
unexpected_property: true,
170+
} as unknown as RawEnvironment);
171+
172+
expect(() => readConfig("wrangler.toml", {}))
173+
.toThrowErrorMatchingInlineSnapshot(`
174+
[Error: Extending with configuration found in .wrangler/config/extra.json.
175+
- Expected "compatibility_date" to be of type string but got 2021.]
176+
`);
177+
expect(std).toMatchInlineSnapshot(`
178+
Object {
179+
"debug": "",
180+
"err": "",
181+
"info": "",
182+
"out": "",
183+
"warn": "▲ [WARNING] Extending with configuration found in .wrangler/config/extra.json.
184+
185+
- Unexpected fields found in extended config field: \\"unexpected_property\\"
186+
187+
",
188+
}
189+
`);
190+
});
191+
192+
it("should override the selected named environment", () => {
193+
const main = "src/index.ts";
194+
const resolvedMain = path.resolve(process.cwd(), main);
195+
196+
writeWranglerToml({
197+
main,
198+
env: {
199+
prod: {
200+
compatibility_date: "2021-01-01",
201+
logpush: true,
202+
},
203+
dev: {
204+
compatibility_date: "2022-02-02",
205+
no_bundle: true,
206+
},
207+
},
208+
});
209+
writeExtraJson({
210+
compatibility_date: "2024-11-01",
211+
compatibility_flags: ["nodejs_compat"],
212+
no_bundle: true,
213+
});
214+
215+
const prodConfig = readConfig("wrangler.toml", { env: "prod" });
216+
expect(prodConfig).toEqual(
217+
expect.objectContaining({
218+
compatibility_date: "2024-11-01",
219+
compatibility_flags: ["nodejs_compat"],
220+
configPath: "wrangler.toml",
221+
main: resolvedMain,
222+
logpush: true,
223+
no_bundle: true,
224+
})
225+
);
226+
227+
const devConfig = readConfig("wrangler.toml", { env: "dev" });
228+
expect(devConfig).toEqual(
229+
expect.objectContaining({
230+
compatibility_date: "2024-11-01",
231+
compatibility_flags: ["nodejs_compat"],
232+
configPath: "wrangler.toml",
233+
main: resolvedMain,
234+
no_bundle: true,
235+
})
236+
);
237+
});
238+
});
46239
});
47240

48241
describe("normalizeAndValidateConfig()", () => {

packages/wrangler/src/__tests__/helpers/write-wrangler-toml.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as fs from "fs";
22
import TOML from "@iarna/toml";
3-
import type { RawConfig } from "../../config";
3+
import { ensureDirectoryExistsSync } from "../../utils/filesystem";
4+
import type { RawConfig, RawEnvironment } from "../../config";
45

56
/** Write a mock wrangler.toml file to disk. */
67
export function writeWranglerToml(
@@ -33,3 +34,11 @@ export function writeWranglerJson(
3334
"utf-8"
3435
);
3536
}
37+
38+
export function writeExtraJson(
39+
config: RawEnvironment = {},
40+
path = "./.wrangler/config/extra.json"
41+
) {
42+
ensureDirectoryExistsSync(path);
43+
fs.writeFileSync(path, JSON.stringify(config), "utf-8");
44+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { existsSync } from "node:fs";
2+
import { dirname, relative, resolve } from "node:path";
3+
import { defu } from "defu";
4+
import { UserError } from "../errors";
5+
import { logger } from "../logger";
6+
import { parseJSONC, readFileSync } from "../parse";
7+
import { Diagnostics } from "./diagnostics";
8+
import { normalizeAndValidateEnvironment } from "./validation";
9+
import { validateAdditionalProperties } from "./validation-helpers";
10+
import type { Config } from "./config";
11+
import type { Environment } from "./environment";
12+
13+
/**
14+
* Merge additional configuration loaded from `.wrangler/config/extra.json`,
15+
* if it exists, into the user provided configuration.
16+
*/
17+
export function extendConfiguration(
18+
configPath: string | undefined,
19+
userConfig: Config,
20+
hideWarnings: boolean
21+
): Config {
22+
// Handle extending the user configuration
23+
const extraPath = getExtraConfigPath(configPath && dirname(configPath));
24+
const extra = loadExtraConfig(extraPath);
25+
if (extra === undefined) {
26+
return userConfig;
27+
}
28+
29+
const { config, diagnostics } = extra;
30+
if (!hideWarnings && !diagnostics.hasWarnings() && !diagnostics.hasErrors()) {
31+
logger.info(diagnostics.description);
32+
}
33+
if (diagnostics.hasWarnings() && !hideWarnings) {
34+
logger.warn(diagnostics.renderWarnings());
35+
}
36+
if (diagnostics.hasErrors()) {
37+
throw new UserError(diagnostics.renderErrors());
38+
}
39+
return defu<Config, [Config]>(config, userConfig);
40+
}
41+
42+
/**
43+
* Get the path to a file that might contain additional configuration to be merged into the user's configuration.
44+
*
45+
* This supports the case where a custom build tool wants to extend the user's configuration as well as pre-bundled files.
46+
*/
47+
function getExtraConfigPath(projectRoot: string | undefined): string {
48+
return resolve(projectRoot ?? ".", ".wrangler/config/extra.json");
49+
}
50+
51+
/**
52+
* Attempt to load and validate extra config from the `.wrangler/config/extra.json` file if it exists.
53+
*/
54+
function loadExtraConfig(configPath: string):
55+
| {
56+
config: Environment;
57+
diagnostics: Diagnostics;
58+
}
59+
| undefined {
60+
if (!existsSync(configPath)) {
61+
return undefined;
62+
}
63+
64+
const diagnostics = new Diagnostics(
65+
`Extending with configuration found in ${relative(process.cwd(), configPath)}.`
66+
);
67+
const raw = parseJSONC<Environment>(readFileSync(configPath), configPath);
68+
const config = normalizeAndValidateEnvironment(
69+
diagnostics,
70+
configPath,
71+
raw,
72+
/* isDispatchNamespace */ false
73+
);
74+
75+
validateAdditionalProperties(
76+
diagnostics,
77+
"extended config",
78+
Object.keys(raw),
79+
[...Object.keys(config)]
80+
);
81+
82+
return { config, diagnostics };
83+
}

packages/wrangler/src/config/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getFlag } from "../experimental-flags";
77
import { logger } from "../logger";
88
import { EXIT_CODE_INVALID_PAGES_CONFIG } from "../pages/errors";
99
import { parseJSONC, parseTOML, readFileSync } from "../parse";
10+
import { extendConfiguration } from "./extra";
1011
import { isPagesConfig, normalizeAndValidateConfig } from "./validation";
1112
import { validatePagesConfig } from "./validation-pages";
1213
import type { CfWorkerInit } from "../deployment-bundle/worker";
@@ -151,7 +152,7 @@ export function readConfig(
151152

152153
applyPythonConfig(config, args);
153154

154-
return config;
155+
return extendConfiguration(configPath, config, hideWarnings);
155156
}
156157

157158
/**

packages/wrangler/src/config/validation.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,7 +1028,7 @@ const validateTailConsumers: ValidatorFn = (diagnostics, field, value) => {
10281028
/**
10291029
* Validate top-level environment configuration and return the normalized values.
10301030
*/
1031-
function normalizeAndValidateEnvironment(
1031+
export function normalizeAndValidateEnvironment(
10321032
diagnostics: Diagnostics,
10331033
configPath: string | undefined,
10341034
topLevelEnv: RawEnvironment,
@@ -1037,7 +1037,7 @@ function normalizeAndValidateEnvironment(
10371037
/**
10381038
* Validate the named environment configuration and return the normalized values.
10391039
*/
1040-
function normalizeAndValidateEnvironment(
1040+
export function normalizeAndValidateEnvironment(
10411041
diagnostics: Diagnostics,
10421042
configPath: string | undefined,
10431043
rawEnv: RawEnvironment,
@@ -1050,7 +1050,7 @@ function normalizeAndValidateEnvironment(
10501050
/**
10511051
* Validate the named environment configuration and return the normalized values.
10521052
*/
1053-
function normalizeAndValidateEnvironment(
1053+
export function normalizeAndValidateEnvironment(
10541054
diagnostics: Diagnostics,
10551055
configPath: string | undefined,
10561056
rawEnv: RawEnvironment,
@@ -1060,7 +1060,7 @@ function normalizeAndValidateEnvironment(
10601060
isLegacyEnv?: boolean,
10611061
rawConfig?: RawConfig
10621062
): Environment;
1063-
function normalizeAndValidateEnvironment(
1063+
export function normalizeAndValidateEnvironment(
10641064
diagnostics: Diagnostics,
10651065
configPath: string | undefined,
10661066
rawEnv: RawEnvironment,

0 commit comments

Comments
 (0)