Skip to content

Commit

Permalink
feat: add support for redirecting Wrangler to a generated config when…
Browse files Browse the repository at this point in the history
… 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"
  }
  ```
  • Loading branch information
petebacondarwin committed Dec 9, 2024
1 parent a29a41c commit e251387
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 49 deletions.
81 changes: 81 additions & 0 deletions .changeset/thin-pots-camp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
"wrangler": minor
---

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"
}
```
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import path from "node:path";
import { normalizeAndValidateConfig } from "../config/validation";
import { normalizeAndValidateConfig } from "../../config/validation";
import {
generateRawConfigForPages,
generateRawEnvConfigForPages,
} from "./helpers/generate-wrangler-config";
import type { RawConfig, RawEnvironment } from "../config";
} from "../helpers/generate-wrangler-config";
import type { RawConfig, RawEnvironment } from "../../config";

describe("normalizeAndValidateConfig()", () => {
describe("Pages configuration", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import path from "node:path";
import { readConfig } from "../config";
import { normalizeAndValidateConfig } from "../config/validation";
import { run } from "../experimental-flags";
import { normalizeString } from "./helpers/normalize";
import { runInTempDir } from "./helpers/run-in-tmp";
import { writeWranglerConfig } from "./helpers/write-wrangler-config";
import { readConfig } from "../../config";
import { normalizeAndValidateConfig } from "../../config/validation";
import { run } from "../../experimental-flags";
import { normalizeString } from "../helpers/normalize";
import { runInTempDir } from "../helpers/run-in-tmp";
import { writeWranglerConfig } from "../helpers/write-wrangler-config";
import type {
ConfigFields,
RawConfig,
RawDevConfig,
RawEnvironment,
} from "../config";
} from "../../config";

describe("readConfig()", () => {
runInTempDir();
Expand Down
218 changes: 218 additions & 0 deletions packages/wrangler/src/__tests__/config/findWranglerConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import path from "node:path";
import { findWranglerConfig } from "../../config/config-helpers";
import { mockConsoleMethods } from "../helpers/mock-console";
import { runInTempDir } from "../helpers/run-in-tmp";
import { seed } from "../helpers/seed";

describe("config findWranglerConfig()", () => {
runInTempDir();
const std = mockConsoleMethods();
const NO_LOGS = { debug: "", err: "", info: "", out: "", warn: "" };

describe("(useRedirect: false)", () => {
it.each(["toml", "json", "jsonc"])(
"should find the nearest wrangler.%s to the current working directory",
async (ext) => {
await seed({
[`wrangler.${ext}`]: "DUMMY",
[`foo/wrangler.${ext}`]: "DUMMY",
[`foo/bar/wrangler.${ext}`]: "DUMMY",
[`foo/bar/qux/holder.txt`]: "DUMMY",
});
expect(findWranglerConfig(".")).toEqual(
path.resolve(`wrangler.${ext}`)
);
expect(findWranglerConfig("./foo")).toEqual(
path.resolve(`foo/wrangler.${ext}`)
);
expect(findWranglerConfig("./foo/bar")).toEqual(
path.resolve(`foo/bar/wrangler.${ext}`)
);
expect(findWranglerConfig("./foo/bar/qux")).toEqual(
path.resolve(`foo/bar/wrangler.${ext}`)
);
expect(std).toEqual(NO_LOGS);
}
);

describe.each([
["json", "jsonc"],
["json", "toml"],
["jsonc", "toml"],
])("should prefer the wrangler.%s over wrangler.%s", (ext1, ext2) => {
it("in the same directory", async () => {
await seed({
[`wrangler.${ext1}`]: "DUMMY",
[`wrangler.${ext2}`]: "DUMMY",
});
expect(findWranglerConfig(".")).toEqual(
path.resolve(`wrangler.${ext1}`)
);
expect(std).toEqual(NO_LOGS);
});

it("in different directories", async () => {
await seed({
[`wrangler.${ext1}`]: "DUMMY",
[`foo/wrangler.${ext2}`]: "DUMMY",
});
expect(findWranglerConfig("./foo")).toEqual(
path.resolve(`wrangler.${ext1}`)
);
expect(std).toEqual(NO_LOGS);
});
});

it("should return user config path even if a deploy config is found", async () => {
await seed({
[`wrangler.toml`]: "DUMMY",
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
[`dist/wrangler.json`]: "DUMMY",
});
expect(findWranglerConfig(".", { useRedirect: false })).toEqual(
path.resolve(`wrangler.toml`)
);
expect(std).toEqual(NO_LOGS);
});
});

describe("(useRedirect: true)", () => {
it("should return redirected config path if no user config and a deploy config is found", async () => {
await seed({
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
[`dist/wrangler.json`]: "DUMMY",
["foo/holder.txt"]: "DUMMY",
});
expect(findWranglerConfig(".", { useRedirect: true })).toEqual(
path.resolve(`dist/wrangler.json`)
);
expect(findWranglerConfig("./foo", { useRedirect: true })).toEqual(
path.resolve(`dist/wrangler.json`)
);
expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"info": "",
"out": "",
"warn": "▲ [WARNING] Using redirected Wrangler configuration.
Redirected config path: \\"dist/wrangler.json\\"
Deploy config path: \\".wrangler/deploy/config.json\\"
Original config path: \\"<no user config found>\\"
▲ [WARNING] Using redirected Wrangler configuration.
Redirected config path: \\"dist/wrangler.json\\"
Deploy config path: \\".wrangler/deploy/config.json\\"
Original config path: \\"<no user config found>\\"
",
}
`);
});

it("should return redirected config path if matching user config and a deploy config is found", async () => {
await seed({
[`wrangler.toml`]: "DUMMY",
[".wrangler/deploy/config.json"]: `{"configPath": "../../dist/wrangler.json" }`,
[`dist/wrangler.json`]: "DUMMY",
["foo/holder.txt"]: "DUMMY",
});
expect(findWranglerConfig(".", { useRedirect: true })).toEqual(
path.resolve(`dist/wrangler.json`)
);
expect(findWranglerConfig("./foo", { useRedirect: true })).toEqual(
path.resolve(`dist/wrangler.json`)
);
expect(std).toMatchInlineSnapshot(`
Object {
"debug": "",
"err": "",
"info": "",
"out": "",
"warn": "▲ [WARNING] Using redirected Wrangler configuration.
Redirected config path: \\"dist/wrangler.json\\"
Deploy config path: \\".wrangler/deploy/config.json\\"
Original config path: \\"wrangler.toml\\"
▲ [WARNING] Using redirected Wrangler configuration.
Redirected config path: \\"dist/wrangler.json\\"
Deploy config path: \\".wrangler/deploy/config.json\\"
Original config path: \\"wrangler.toml\\"
",
}
`);
});

it("should error if deploy config is not valid JSON", async () => {
await seed({
[".wrangler/deploy/config.json"]: `INVALID JSON`,
});
expect(() =>
findWranglerConfig(".", { useRedirect: true })
).toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to load the deploy config at .wrangler/deploy/config.json]`
);
expect(std).toEqual(NO_LOGS);
});

it("should error if deploy config does not contain a `configPath` property", async () => {
await seed({
[".wrangler/deploy/config.json"]: `{}`,
});
expect(() => findWranglerConfig(".", { useRedirect: true }))
.toThrowErrorMatchingInlineSnapshot(`
[Error: A redirect config was found at ".wrangler/deploy/config.json".
But this is not valid - the required "configPath" property was not found.
Instead this file contains:
\`\`\`
{}
\`\`\`]
`);
expect(std).toEqual(NO_LOGS);
});

it("should error if redirected config file does not exist", async () => {
await seed({
[".wrangler/deploy/config.json"]: `{ "configPath": "missing/wrangler.json" }`,
});
expect(() => findWranglerConfig(".", { useRedirect: true }))
.toThrowErrorMatchingInlineSnapshot(`
[Error: There is a redirect configuration at ".wrangler/deploy/config.json".
But the config path it points to, ".wrangler/deploy/missing/wrangler.json", does not exist.]
`);
expect(std).toEqual(NO_LOGS);
});

it("should error if deploy config file and user config file do not have the same base path", async () => {
await seed({
[`foo/wrangler.toml`]: "DUMMY",
["foo/bar/.wrangler/deploy/config.json"]: `{ "configPath": "../../dist/wrangler.json" }`,
[`foo/bar/dist/wrangler.json`]: "DUMMY",

[`bar/foo/wrangler.toml`]: "DUMMY",
["bar/.wrangler/deploy/config.json"]: `{ "configPath": "../../dist/wrangler.json" }`,
[`bar/dist/wrangler.json`]: "DUMMY",
});
expect(() => findWranglerConfig("foo/bar", { useRedirect: true }))
.toThrowErrorMatchingInlineSnapshot(`
[Error: Found both a user config file at "foo/wrangler.toml"
and a redirect config file at "foo/bar/.wrangler/deploy/config.json".
But these do not share the same base path so it is not clear which should be used.]
`);
expect(() => findWranglerConfig("bar/foo", { useRedirect: true }))
.toThrowErrorMatchingInlineSnapshot(`
[Error: Found both a user config file at "bar/foo/wrangler.toml"
and a redirect config file at "bar/.wrangler/deploy/config.json".
But these do not share the same base path so it is not clear which should be used.]
`);
expect(std).toEqual(NO_LOGS);
});
});
});
Loading

0 comments on commit e251387

Please sign in to comment.