diff --git a/.changeset/wild-donkeys-shake.md b/.changeset/wild-donkeys-shake.md new file mode 100644 index 000000000000..8641ac148af3 --- /dev/null +++ b/.changeset/wild-donkeys-shake.md @@ -0,0 +1,17 @@ +--- +"wrangler": patch +--- + +feat: read `vars` overrides from a local file for `wrangler dev` + +The `vars` bindings can be specified in the `wrangler.toml` configuration file. +But "secret" `vars` are usually only provided at the server - +either by creating them in the Dashboard UI, or using the `wrangler secret` command. + +It is useful during development, to provide these types of variable locally. +When running `wrangler dev` we will look for a file called `.dev.vars`, situated +next to the `wrangler.toml` file (or in the current working directory if there is no +`wrangler.toml`). Any values in this file, formatted like a `dotenv` file, will add to +or override `vars` bindings provided in the `wrangler.toml`. + +Related to #190 diff --git a/package-lock.json b/package-lock.json index 03d57f80f73d..597c628ea673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18973,6 +18973,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -20664,6 +20673,7 @@ "supports-color": "^9.2.2", "timeago.js": "^4.0.2", "tmp-promise": "^3.0.3", + "ts-dedent": "^2.2.0", "undici": "^4.15.1", "update-check": "^1.5.4", "ws": "^8.5.0", @@ -34429,6 +34439,12 @@ "version": "2.0.2", "dev": true }, + "ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true + }, "tsconfig-paths": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", @@ -35490,7 +35506,7 @@ "cmd-shim": "^4.1.0", "command-exists": "^1.2.9", "devtools-protocol": "^0.0.955664", - "dotenv": "*", + "dotenv": "^16.0.0", "esbuild": "0.14.34", "execa": "^6.1.0", "faye-websocket": "^0.11.4", @@ -35523,6 +35539,7 @@ "supports-color": "^9.2.2", "timeago.js": "^4.0.2", "tmp-promise": "^3.0.3", + "ts-dedent": "^2.2.0", "undici": "^4.15.1", "update-check": "^1.5.4", "ws": "^8.5.0", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 7d3cb607cbe4..01f272f17adc 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -96,6 +96,7 @@ "supports-color": "^9.2.2", "timeago.js": "^4.0.2", "tmp-promise": "^3.0.3", + "ts-dedent": "^2.2.0", "undici": "^4.15.1", "update-check": "^1.5.4", "ws": "^8.5.0", diff --git a/packages/wrangler/src/__tests__/dev.test.tsx b/packages/wrangler/src/__tests__/dev.test.tsx index 5bb3c680fffc..0da5cf944c8d 100644 --- a/packages/wrangler/src/__tests__/dev.test.tsx +++ b/packages/wrangler/src/__tests__/dev.test.tsx @@ -1,6 +1,6 @@ import * as fs from "node:fs"; -import { readFileSync } from "node:fs"; import patchConsole from "patch-console"; +import dedent from "ts-dedent"; import Dev from "../dev/dev"; import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; import { setMockResponse, unsetAllMocks } from "./helpers/mock-cfetch"; @@ -382,7 +382,7 @@ describe("wrangler dev", () => { await runWrangler("dev index.js"); - expect(readFileSync("index.js", "utf-8")).toMatchInlineSnapshot( + expect(fs.readFileSync("index.js", "utf-8")).toMatchInlineSnapshot( `"export default { fetch(){ return new Response(123) } }"` ); @@ -410,7 +410,7 @@ describe("wrangler dev", () => { await runWrangler("dev index.js"); - expect(readFileSync("index.js", "utf-8")).toMatchInlineSnapshot(` + expect(fs.readFileSync("index.js", "utf-8")).toMatchInlineSnapshot(` "export default { fetch(){ return new Response(123) } } " `); @@ -615,6 +615,55 @@ describe("wrangler dev", () => { expect(std.err).toMatchInlineSnapshot(`""`); }); }); + + describe(".dev.vars", () => { + it("should override `vars` bindings from `wrangler.toml` with values in `.dev.vars`", async () => { + fs.writeFileSync("index.js", `export default {};`); + + const localVarsEnvContent = dedent` + # Preceding comment + VAR_1="var #1 value" # End of line comment + VAR_3="var #3 value" + VAR_MULTI_LINE_1="A: line 1 + line 2" + VAR_MULTI_LINE_2="B: line 1\\nline 2" + EMPTY= + UNQUOTED= unquoted value + `; + fs.writeFileSync(".dev.vars", localVarsEnvContent, "utf8"); + + writeWranglerToml({ + main: "index.js", + vars: { + VAR_1: "original value 1", + VAR_2: "original value 2", // should not get overridden + VAR_3: "original value 3", + VAR_MULTI_LINE_1: "original multi-line 1", + VAR_MULTI_LINE_2: "original multi-line 2", + EMPTY: "original empty", + UNQUOTED: "original unquoted", + }, + }); + await runWrangler("dev"); + const varBindings: Record = (Dev as jest.Mock).mock + .calls[0][0].bindings.vars; + + expect(varBindings).toEqual({ + VAR_1: "var #1 value", + VAR_2: "original value 2", + VAR_3: "var #3 value", + VAR_MULTI_LINE_1: "A: line 1\nline 2", + VAR_MULTI_LINE_2: "B: line 1\nline 2", + EMPTY: "", + UNQUOTED: "unquoted value", // Note that whitespace is trimmed + }); + expect(std.out).toMatchInlineSnapshot( + `"Add vars defined in \\".dev.vars\\" to the \\"vars\\" bindings."` + ); + expect(std.warn).toMatchInlineSnapshot(`""`); + expect(std.err).toMatchInlineSnapshot(`""`); + }); + }); }); function mockGetZones(domain: string, zones: { id: string }[] = []) { diff --git a/packages/wrangler/src/dev/dev-vars.ts b/packages/wrangler/src/dev/dev-vars.ts new file mode 100644 index 000000000000..dcd6afda2084 --- /dev/null +++ b/packages/wrangler/src/dev/dev-vars.ts @@ -0,0 +1,38 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import dotenv from "dotenv"; +import { logger } from "../logger"; +import type { Config } from "../config"; + +/** + * Get the Worker `vars` bindings for a `wrangler dev` instance of a Worker. + * + * The `vars` bindings can be specified in the `wrangler.toml` configuration file. + * But "secret" `vars` are usually only provided at the server - + * either by creating them in the Dashboard UI, or using the `wrangler secret` command. + * + * It is useful during development, to provide these types of variable locally. + * When running `wrangler dev` we will look for a file called `.dev.vars`, situated + * next to the `wrangler.toml` file (or in the current working directory if there is no + * `wrangler.toml`). + * + * Any values in this file, formatted like a `dotenv` file, will add to or override `vars` + * bindings provided in the `wrangler.toml`. + */ +export function getVarsForDev(config: Config): Config["vars"] { + const configDir = path.resolve(path.dirname(config.configPath ?? ".")); + const devVarsPath = path.resolve(configDir, ".dev.vars"); + if (fs.existsSync(devVarsPath)) { + const devVarsRelativePath = path.relative(process.cwd(), devVarsPath); + logger.log( + `Add vars defined in "${devVarsRelativePath}" to the "vars" bindings.` + ); + const devVars = dotenv.parse(fs.readFileSync(devVarsPath, "utf8")); + return { + ...config.vars, + ...devVars, + }; + } else { + return config.vars; + } +} diff --git a/packages/wrangler/src/index.tsx b/packages/wrangler/src/index.tsx index 8b8f57df0018..cb58c86932df 100644 --- a/packages/wrangler/src/index.tsx +++ b/packages/wrangler/src/index.tsx @@ -16,6 +16,7 @@ import { fetchResult } from "./cfetch"; import { findWranglerToml, readConfig } from "./config"; import { createWorkerUploadForm } from "./create-worker-upload-form"; import Dev from "./dev/dev"; +import { getVarsForDev } from "./dev/dev-vars"; import { confirm, prompt } from "./dialogs"; import { getEntry } from "./entry"; import { DeprecationError } from "./errors"; @@ -746,7 +747,7 @@ export async function main(argv: string[]): Promise { type: "boolean", }) .option("node-compat", { - describe: "Enable node.js compaitibility", + describe: "Enable node.js compatibility", type: "boolean", }); }, @@ -937,7 +938,7 @@ export async function main(argv: string[]): Promise { }; } ), - vars: config.vars, + vars: getVarsForDev(config), wasm_modules: config.wasm_modules, text_blobs: config.text_blobs, data_blobs: config.data_blobs, @@ -1044,7 +1045,7 @@ export async function main(argv: string[]): Promise { type: "boolean", }) .option("node-compat", { - describe: "Enable node.js compaitibility", + describe: "Enable node.js compatibility", type: "boolean", }) .option("dry-run", {