Skip to content

Commit 5a91f5b

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. ### Affected commands The commands that use this feature are: - `wrangler deploy` - `wrangler dev` - `wrangler versions upload` - `wrangler versions deploy` ### Config redirect file 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. ### Custom build tool example 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 bea6558 commit 5a91f5b

File tree

19 files changed

+465
-55
lines changed

19 files changed

+465
-55
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";
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+
});

packages/wrangler/src/api/pages/deploy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,11 @@ export async function deploy({
160160
let config: Config | undefined;
161161

162162
try {
163-
config = readConfig(undefined, { ...args, env }, true);
163+
config = readConfig(
164+
undefined,
165+
{ ...args, env },
166+
{ requirePagesConfig: true }
167+
);
164168
} catch (err) {
165169
if (
166170
!(

0 commit comments

Comments
 (0)