Skip to content

Commit e251387

Browse files
feat: add support for redirecting Wrangler to a generated config when running deploy commands
This new feature is designed for build tools and frameworks to provide a deploy-specific configuration, which Wrangler can use instead of user configuration when running deploy commands. It is not expected that developers of Workers will need to use this feature directly. The commands that use this feature are: - `wrangler deploy` - `wrangler dev` - `wrangler versions upload` - `wrangler versions deploy` When running these commands, Wrangler will look up the directory tree from the current working directory for a file at the path `.wrangler/deploy/config.json`. This file must contain only a single JSON object of the form: ```json { "configPath": "../../path/to/wrangler.json" } ``` When this file exists Wrangler will use the `configPath` (relative to the `deploy.json` file) to find an alternative Wrangler configuration file to load and use as part of this command. When this happens Wrangler will display a warning to the user to indicate that the configuration has been redirected to a different file than the user's configuration file. A common approach that a build tool might choose to implement. - The user writes code that uses Cloudflare Workers resources, configured via a user `wrangler.toml` file. ```toml name = "my-worker" main = "src/index.ts" [[kv_namespaces]] binding = "<BINDING_NAME1>" id = "<NAMESPACE_ID1>" ``` Note that this configuration points `main` at user code entry-point. - The user runs a custom build, which might read the `wrangler.toml` to find the entry-point: ```bash > my-tool build ``` - This tool generates a `dist` directory that contains both compiled code and a new deployment configuration file, but also a `.wrangler/deploy/config.json` file that redirects Wrangler to this new deployment configuration file: ```plain - dist - index.js - wrangler.json - .wrangler - deploy - config.json ``` The `dist/wrangler.json` will contain: ```json { "name": "my-worker", "main": "./index.js", "kv_namespaces": [{ "binding": "<BINDING_NAME1>", "id": "<NAMESPACE_ID1>" }] } ``` And the `.wrangler/deploy/config.json` will contain: ```json { "configPath": "../../dist/wrangler.json" } ```
1 parent a29a41c commit e251387

File tree

14 files changed

+457
-49
lines changed

14 files changed

+457
-49
lines changed

.changeset/thin-pots-camp.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
"wrangler": minor
3+
---
4+
5+
feat: add support for redirecting Wrangler to a generated config when running deploy commands
6+
7+
This new feature is designed for build tools and frameworks to provide a deploy-specific configuration,
8+
which Wrangler can use instead of user configuration when running deploy commands.
9+
It is not expected that developers of Workers will need to use this feature directly.
10+
11+
### Affected commands
12+
13+
The commands that use this feature are:
14+
15+
- `wrangler deploy`
16+
- `wrangler dev`
17+
- `wrangler versions upload`
18+
- `wrangler versions deploy`
19+
20+
### Config redirect file
21+
22+
When running these commands, Wrangler will look up the directory tree from the current working directory for a file at the path `.wrangler/deploy/config.json`. This file must contain only a single JSON object of the form:
23+
24+
```json
25+
{ "configPath": "../../path/to/wrangler.json" }
26+
```
27+
28+
When this file exists Wrangler will use the `configPath` (relative to the `deploy.json` file) to find an alternative Wrangler configuration file to load and use as part of this command.
29+
30+
When this happens Wrangler will display a warning to the user to indicate that the configuration has been redirected to a different file than the user's configuration file.
31+
32+
### Custom build tool example
33+
34+
A common approach that a build tool might choose to implement.
35+
36+
- The user writes code that uses Cloudflare Workers resources, configured via a user `wrangler.toml` file.
37+
38+
```toml
39+
name = "my-worker"
40+
main = "src/index.ts"
41+
[[kv_namespaces]]
42+
binding = "<BINDING_NAME1>"
43+
id = "<NAMESPACE_ID1>"
44+
```
45+
46+
Note that this configuration points `main` at user code entry-point.
47+
48+
- The user runs a custom build, which might read the `wrangler.toml` to find the entry-point:
49+
50+
```bash
51+
> my-tool build
52+
```
53+
54+
- This tool generates a `dist` directory that contains both compiled code and a new deployment configuration file, but also a `.wrangler/deploy/config.json` file that redirects Wrangler to this new deployment configuration file:
55+
56+
```plain
57+
- dist
58+
- index.js
59+
- wrangler.json
60+
- .wrangler
61+
- deploy
62+
- config.json
63+
```
64+
65+
The `dist/wrangler.json` will contain:
66+
67+
```json
68+
{
69+
"name": "my-worker",
70+
"main": "./index.js",
71+
"kv_namespaces": [{ "binding": "<BINDING_NAME1>", "id": "<NAMESPACE_ID1>" }]
72+
}
73+
```
74+
75+
And the `.wrangler/deploy/config.json` will contain:
76+
77+
```json
78+
{
79+
"configPath": "../../dist/wrangler.json"
80+
}
81+
```

packages/wrangler/src/__tests__/configuration.pages.test.ts renamed to packages/wrangler/src/__tests__/config/configuration.pages.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import path from "node:path";
2-
import { normalizeAndValidateConfig } from "../config/validation";
2+
import { normalizeAndValidateConfig } from "../../config/validation";
33
import {
44
generateRawConfigForPages,
55
generateRawEnvConfigForPages,
6-
} from "./helpers/generate-wrangler-config";
7-
import type { RawConfig, RawEnvironment } from "../config";
6+
} from "../helpers/generate-wrangler-config";
7+
import type { RawConfig, RawEnvironment } from "../../config";
88

99
describe("normalizeAndValidateConfig()", () => {
1010
describe("Pages configuration", () => {

packages/wrangler/src/__tests__/configuration.test.ts renamed to packages/wrangler/src/__tests__/config/configuration.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import path from "node:path";
2-
import { readConfig } from "../config";
3-
import { normalizeAndValidateConfig } from "../config/validation";
4-
import { run } from "../experimental-flags";
5-
import { normalizeString } from "./helpers/normalize";
6-
import { runInTempDir } from "./helpers/run-in-tmp";
7-
import { writeWranglerConfig } from "./helpers/write-wrangler-config";
2+
import { readConfig } from "../../config";
3+
import { normalizeAndValidateConfig } from "../../config/validation";
4+
import { run } from "../../experimental-flags";
5+
import { normalizeString } from "../helpers/normalize";
6+
import { runInTempDir } from "../helpers/run-in-tmp";
7+
import { writeWranglerConfig } from "../helpers/write-wrangler-config";
88
import type {
99
ConfigFields,
1010
RawConfig,
1111
RawDevConfig,
1212
RawEnvironment,
13-
} from "../config";
13+
} from "../../config";
1414

1515
describe("readConfig()", () => {
1616
runInTempDir();
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import path from "node:path";
2+
import { findWranglerConfig } from "../../config/config-helpers";
3+
import { mockConsoleMethods } from "../helpers/mock-console";
4+
import { runInTempDir } from "../helpers/run-in-tmp";
5+
import { seed } from "../helpers/seed";
6+
7+
describe("config findWranglerConfig()", () => {
8+
runInTempDir();
9+
const std = mockConsoleMethods();
10+
const NO_LOGS = { debug: "", err: "", info: "", out: "", warn: "" };
11+
12+
describe("(useRedirect: false)", () => {
13+
it.each(["toml", "json", "jsonc"])(
14+
"should find the nearest wrangler.%s to the current working directory",
15+
async (ext) => {
16+
await seed({
17+
[`wrangler.${ext}`]: "DUMMY",
18+
[`foo/wrangler.${ext}`]: "DUMMY",
19+
[`foo/bar/wrangler.${ext}`]: "DUMMY",
20+
[`foo/bar/qux/holder.txt`]: "DUMMY",
21+
});
22+
expect(findWranglerConfig(".")).toEqual(
23+
path.resolve(`wrangler.${ext}`)
24+
);
25+
expect(findWranglerConfig("./foo")).toEqual(
26+
path.resolve(`foo/wrangler.${ext}`)
27+
);
28+
expect(findWranglerConfig("./foo/bar")).toEqual(
29+
path.resolve(`foo/bar/wrangler.${ext}`)
30+
);
31+
expect(findWranglerConfig("./foo/bar/qux")).toEqual(
32+
path.resolve(`foo/bar/wrangler.${ext}`)
33+
);
34+
expect(std).toEqual(NO_LOGS);
35+
}
36+
);
37+
38+
describe.each([
39+
["json", "jsonc"],
40+
["json", "toml"],
41+
["jsonc", "toml"],
42+
])("should prefer the wrangler.%s over wrangler.%s", (ext1, ext2) => {
43+
it("in the same directory", async () => {
44+
await seed({
45+
[`wrangler.${ext1}`]: "DUMMY",
46+
[`wrangler.${ext2}`]: "DUMMY",
47+
});
48+
expect(findWranglerConfig(".")).toEqual(
49+
path.resolve(`wrangler.${ext1}`)
50+
);
51+
expect(std).toEqual(NO_LOGS);
52+
});
53+
54+
it("in different directories", async () => {
55+
await seed({
56+
[`wrangler.${ext1}`]: "DUMMY",
57+
[`foo/wrangler.${ext2}`]: "DUMMY",
58+
});
59+
expect(findWranglerConfig("./foo")).toEqual(
60+
path.resolve(`wrangler.${ext1}`)
61+
);
62+
expect(std).toEqual(NO_LOGS);
63+
});
64+
});
65+
66+
it("should return user config path even if a deploy config is found", async () => {
67+
await seed({
68+
[`wrangler.toml`]: "DUMMY",
69+
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
70+
[`dist/wrangler.json`]: "DUMMY",
71+
});
72+
expect(findWranglerConfig(".", { useRedirect: false })).toEqual(
73+
path.resolve(`wrangler.toml`)
74+
);
75+
expect(std).toEqual(NO_LOGS);
76+
});
77+
});
78+
79+
describe("(useRedirect: true)", () => {
80+
it("should return redirected config path if no user config and a deploy config is found", async () => {
81+
await seed({
82+
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
83+
[`dist/wrangler.json`]: "DUMMY",
84+
["foo/holder.txt"]: "DUMMY",
85+
});
86+
expect(findWranglerConfig(".", { useRedirect: true })).toEqual(
87+
path.resolve(`dist/wrangler.json`)
88+
);
89+
expect(findWranglerConfig("./foo", { useRedirect: true })).toEqual(
90+
path.resolve(`dist/wrangler.json`)
91+
);
92+
expect(std).toMatchInlineSnapshot(`
93+
Object {
94+
"debug": "",
95+
"err": "",
96+
"info": "",
97+
"out": "",
98+
"warn": "▲ [WARNING] Using redirected Wrangler configuration.
99+
100+
Redirected config path: \\"dist/wrangler.json\\"
101+
Deploy config path: \\".wrangler/deploy/config.json\\"
102+
Original config path: \\"<no user config found>\\"
103+
104+
105+
▲ [WARNING] Using redirected Wrangler configuration.
106+
107+
Redirected config path: \\"dist/wrangler.json\\"
108+
Deploy config path: \\".wrangler/deploy/config.json\\"
109+
Original config path: \\"<no user config found>\\"
110+
111+
",
112+
}
113+
`);
114+
});
115+
116+
it("should return redirected config path if matching user config and a deploy config is found", async () => {
117+
await seed({
118+
[`wrangler.toml`]: "DUMMY",
119+
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
120+
[`dist/wrangler.json`]: "DUMMY",
121+
["foo/holder.txt"]: "DUMMY",
122+
});
123+
expect(findWranglerConfig(".", { useRedirect: true })).toEqual(
124+
path.resolve(`dist/wrangler.json`)
125+
);
126+
expect(findWranglerConfig("./foo", { useRedirect: true })).toEqual(
127+
path.resolve(`dist/wrangler.json`)
128+
);
129+
expect(std).toMatchInlineSnapshot(`
130+
Object {
131+
"debug": "",
132+
"err": "",
133+
"info": "",
134+
"out": "",
135+
"warn": "▲ [WARNING] Using redirected Wrangler configuration.
136+
137+
Redirected config path: \\"dist/wrangler.json\\"
138+
Deploy config path: \\".wrangler/deploy/config.json\\"
139+
Original config path: \\"wrangler.toml\\"
140+
141+
142+
▲ [WARNING] Using redirected Wrangler configuration.
143+
144+
Redirected config path: \\"dist/wrangler.json\\"
145+
Deploy config path: \\".wrangler/deploy/config.json\\"
146+
Original config path: \\"wrangler.toml\\"
147+
148+
",
149+
}
150+
`);
151+
});
152+
153+
it("should error if deploy config is not valid JSON", async () => {
154+
await seed({
155+
[".wrangler/deploy/config.json"]: `INVALID JSON`,
156+
});
157+
expect(() =>
158+
findWranglerConfig(".", { useRedirect: true })
159+
).toThrowErrorMatchingInlineSnapshot(
160+
`[Error: Failed to load the deploy config at .wrangler/deploy/config.json]`
161+
);
162+
expect(std).toEqual(NO_LOGS);
163+
});
164+
165+
it("should error if deploy config does not contain a `configPath` property", async () => {
166+
await seed({
167+
[".wrangler/deploy/config.json"]: `{}`,
168+
});
169+
expect(() => findWranglerConfig(".", { useRedirect: true }))
170+
.toThrowErrorMatchingInlineSnapshot(`
171+
[Error: A redirect config was found at ".wrangler/deploy/config.json".
172+
But this is not valid - the required "configPath" property was not found.
173+
Instead this file contains:
174+
\`\`\`
175+
{}
176+
\`\`\`]
177+
`);
178+
expect(std).toEqual(NO_LOGS);
179+
});
180+
181+
it("should error if redirected config file does not exist", async () => {
182+
await seed({
183+
[".wrangler/deploy/config.json"]: `{ "configPath": "missing/wrangler.json" }`,
184+
});
185+
expect(() => findWranglerConfig(".", { useRedirect: true }))
186+
.toThrowErrorMatchingInlineSnapshot(`
187+
[Error: There is a redirect configuration at ".wrangler/deploy/config.json".
188+
But the config path it points to, ".wrangler/deploy/missing/wrangler.json", does not exist.]
189+
`);
190+
expect(std).toEqual(NO_LOGS);
191+
});
192+
193+
it("should error if deploy config file and user config file do not have the same base path", async () => {
194+
await seed({
195+
[`foo/wrangler.toml`]: "DUMMY",
196+
["foo/bar/.wrangler/deploy/config.json"]: `{ "configPath": "../../dist/wrangler.json" }`,
197+
[`foo/bar/dist/wrangler.json`]: "DUMMY",
198+
199+
[`bar/foo/wrangler.toml`]: "DUMMY",
200+
["bar/.wrangler/deploy/config.json"]: `{ "configPath": "../../dist/wrangler.json" }`,
201+
[`bar/dist/wrangler.json`]: "DUMMY",
202+
});
203+
expect(() => findWranglerConfig("foo/bar", { useRedirect: true }))
204+
.toThrowErrorMatchingInlineSnapshot(`
205+
[Error: Found both a user config file at "foo/wrangler.toml"
206+
and a redirect config file at "foo/bar/.wrangler/deploy/config.json".
207+
But these do not share the same base path so it is not clear which should be used.]
208+
`);
209+
expect(() => findWranglerConfig("bar/foo", { useRedirect: true }))
210+
.toThrowErrorMatchingInlineSnapshot(`
211+
[Error: Found both a user config file at "bar/foo/wrangler.toml"
212+
and a redirect config file at "bar/.wrangler/deploy/config.json".
213+
But these do not share the same base path so it is not clear which should be used.]
214+
`);
215+
expect(std).toEqual(NO_LOGS);
216+
});
217+
});
218+
});

0 commit comments

Comments
 (0)