diff --git a/.changeset/spotty-waves-think.md b/.changeset/spotty-waves-think.md new file mode 100644 index 000000000000..79846adb692c --- /dev/null +++ b/.changeset/spotty-waves-think.md @@ -0,0 +1,11 @@ +--- +"wrangler": patch +--- + +feat: add support for reading build time env variables from a `.env` file + +This change will automatically load up a `.env` file, if found, and apply its +values to the current environment. An example would be to provide a specific +CLOUDFLARE_ACCOUNT_ID value. + +Related to cloudflare#190 diff --git a/examples/local-mode-tests/.env b/examples/local-mode-tests/.env new file mode 100644 index 000000000000..a7bbbab37951 --- /dev/null +++ b/examples/local-mode-tests/.env @@ -0,0 +1 @@ +FOO="the value of foo" diff --git a/examples/local-mode-tests/src/wrangler.dotenv.toml b/examples/local-mode-tests/src/wrangler.dotenv.toml new file mode 100644 index 000000000000..8f8f6b5c050a --- /dev/null +++ b/examples/local-mode-tests/src/wrangler.dotenv.toml @@ -0,0 +1,6 @@ +name = "local-mode-tests" +compatibility_date = "2022-03-27" + +# This custom build command will show whether the FOO +# environment variable was read from the `.env` file. +build.command = "node -e \"console.log(process.env.FOO)\"" diff --git a/examples/local-mode-tests/tests/dotenv.test.ts b/examples/local-mode-tests/tests/dotenv.test.ts new file mode 100644 index 000000000000..6f8f7ffac944 --- /dev/null +++ b/examples/local-mode-tests/tests/dotenv.test.ts @@ -0,0 +1,15 @@ +import { spawnWranglerDev } from "./helpers"; + +it("should use the environment variable from the .env file", async () => { + const { wranglerProcess, fetchWhenReady, terminateProcess } = + spawnWranglerDev("src/module.ts", "src/wrangler.dotenv.toml", 9002); + + try { + await fetchWhenReady("http://localhost"); + expect(wranglerProcess.stdout?.read().toString()).toContain( + "the value of foo" + ); + } finally { + await terminateProcess(); + } +}); diff --git a/examples/local-mode-tests/tests/helpers.ts b/examples/local-mode-tests/tests/helpers.ts new file mode 100644 index 000000000000..28bbf8823f46 --- /dev/null +++ b/examples/local-mode-tests/tests/helpers.ts @@ -0,0 +1,77 @@ +import { spawn } from "node:child_process"; +import { fetch } from "undici"; +import type { Response } from "undici"; + +const isWindows = process.platform === "win32"; + +export async function sleep(ms: number): Promise { + await new Promise((resolvePromise) => setTimeout(resolvePromise, ms)); +} + +/** + * Spawn a child process that is running `wrangler dev`. + * + * @returns two helper functions: + * - `fetchWhenReady()` will run a fetch against the preview Worker, once it is up and running, + * and return its response. + * - `terminateProcess()` send a signal to the `wrangler dev` child process to kill itself. + */ +export function spawnWranglerDev( + srcPath: string, + wranglerTomlPath: string, + port: number +) { + const wranglerProcess = spawn( + "npx", + [ + "wrangler", + "dev", + srcPath, + "--local", + "--config", + wranglerTomlPath, + "--port", + port.toString(), + ], + { + shell: isWindows, + stdio: "pipe", + } + ); + + const fetchWhenReady = async (url: string): Promise => { + const MAX_ATTEMPTS = 50; + const SLEEP_MS = 100; + let attempts = MAX_ATTEMPTS; + while (attempts-- > 0) { + await sleep(SLEEP_MS); + try { + return await fetch(`${url}:${port}`); + } catch {} + } + throw new Error( + `Failed to connect to "${url}:${port}" within ${ + (MAX_ATTEMPTS * SLEEP_MS) / 1000 + } seconds.` + ); + }; + + const terminateProcess = () => { + return new Promise((resolve, reject) => { + wranglerProcess.once("exit", (code) => { + if (!code) { + resolve(code); + } else { + reject(code); + } + }); + wranglerProcess.kill(); + }); + }; + + return { + wranglerProcess, + fetchWhenReady, + terminateProcess, + }; +} diff --git a/examples/local-mode-tests/tests/module.test.ts b/examples/local-mode-tests/tests/module.test.ts index 61466eec77cf..d13a5148097f 100644 --- a/examples/local-mode-tests/tests/module.test.ts +++ b/examples/local-mode-tests/tests/module.test.ts @@ -1,71 +1,16 @@ -import { spawn } from "child_process"; -import { fetch } from "undici"; -import type { ChildProcess } from "child_process"; -import type { Response } from "undici"; - -const waitUntilReady = async (url: string): Promise => { - let response: Response | undefined = undefined; - - while (response === undefined) { - await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); - - try { - response = await fetch(url); - } catch {} - } - - return response as Response; -}; -const isWindows = process.platform === "win32"; - -let wranglerProcess: ChildProcess; - -beforeAll(async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; - - wranglerProcess = spawn( - "npx", - [ - "wrangler", - "dev", - "src/module.ts", - "--local", - "--config", - "src/wrangler.module.toml", - "--port", - "9001", - ], - { - shell: isWindows, - stdio: "inherit", - } - ); -}); - -afterAll(async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; - - await new Promise((resolve, reject) => { - wranglerProcess.once("exit", (code) => { - if (!code) { - resolve(code); - } else { - reject(code); - } - }); - wranglerProcess.kill(); - }); -}); +import { spawnWranglerDev } from "./helpers"; it("renders", async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; + const { fetchWhenReady, terminateProcess } = spawnWranglerDev( + "src/module.ts", + "src/wrangler.module.toml", + 9001 + ); - const response = await waitUntilReady("http://localhost:9001/"); - const text = await response.text(); - expect(text).toMatchInlineSnapshot(` + try { + const response = await fetchWhenReady("http://localhost"); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(` "{ \\"VAR1\\": \\"value1\\", \\"VAR2\\": 123, @@ -76,4 +21,7 @@ it("renders", async () => { \\"data\\": \\"Here be some data\\" }" `); + } finally { + await terminateProcess(); + } }); diff --git a/examples/local-mode-tests/tests/sw.test.ts b/examples/local-mode-tests/tests/sw.test.ts index c1d8605eac5f..80bb07edc256 100644 --- a/examples/local-mode-tests/tests/sw.test.ts +++ b/examples/local-mode-tests/tests/sw.test.ts @@ -1,71 +1,16 @@ -import { spawn } from "child_process"; -import { fetch } from "undici"; -import type { ChildProcess } from "child_process"; -import type { Response } from "undici"; - -const waitUntilReady = async (url: string): Promise => { - let response: Response | undefined = undefined; - - while (response === undefined) { - await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); - - try { - response = await fetch(url); - } catch {} - } - - return response as Response; -}; -const isWindows = process.platform === "win32"; - -let wranglerProcess: ChildProcess; - -beforeAll(async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; - - wranglerProcess = spawn( - "npx", - [ - "wrangler", - "dev", - "src/sw.ts", - "--local", - "--config", - "src/wrangler.sw.toml", - "--port", - "9002", - ], - { - shell: isWindows, - stdio: "inherit", - } - ); -}); - -afterAll(async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; - - await new Promise((resolve, reject) => { - wranglerProcess.once("exit", (code) => { - if (!code) { - resolve(code); - } else { - reject(code); - } - }); - wranglerProcess.kill(); - }); -}); +import { spawnWranglerDev } from "./helpers"; it("renders", async () => { - // These tests break in CI for windows, so we're disabling them for now - if (isWindows) return; + const { fetchWhenReady, terminateProcess } = spawnWranglerDev( + "src/sw.ts", + "src/wrangler.sw.toml", + 9000 + ); - const response = await waitUntilReady("http://localhost:9002/"); - const text = await response.text(); - expect(text).toMatchInlineSnapshot(` + try { + const response = await fetchWhenReady("http://localhost"); + const text = await response.text(); + expect(text).toMatchInlineSnapshot(` "{ \\"VAR1\\": \\"value1\\", \\"VAR2\\": 123, @@ -78,4 +23,7 @@ it("renders", async () => { \\"DATA\\": \\"Here be some data\\" }" `); + } finally { + await terminateProcess(); + } }); diff --git a/package-lock.json b/package-lock.json index 27532b645b15..03d57f80f73d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20637,6 +20637,7 @@ "cmd-shim": "^4.1.0", "command-exists": "^1.2.9", "devtools-protocol": "^0.0.955664", + "dotenv": "^16.0.0", "execa": "^6.1.0", "faye-websocket": "^0.11.4", "finalhandler": "^1.2.0", @@ -20681,6 +20682,15 @@ "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", "dev": true }, + "packages/wrangler/node_modules/dotenv": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "packages/wrangler/node_modules/esbuild": { "version": "0.14.34", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.34.tgz", @@ -35480,6 +35490,7 @@ "cmd-shim": "^4.1.0", "command-exists": "^1.2.9", "devtools-protocol": "^0.0.955664", + "dotenv": "*", "esbuild": "0.14.34", "execa": "^6.1.0", "faye-websocket": "^0.11.4", @@ -35525,6 +35536,12 @@ "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", "dev": true }, + "dotenv": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.0.tgz", + "integrity": "sha512-qD9WU0MPM4SWLPJy/r2Be+2WgQj8plChsyrCNQzW/0WjvcJQiKQJ9mH3ZgB3fxbUUxgc/11ZJ0Fi5KiimWGz2Q==", + "dev": true + }, "esbuild": { "version": "0.14.34", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.34.tgz", diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 3251d95f5774..7d3cb607cbe4 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -69,6 +69,7 @@ "cmd-shim": "^4.1.0", "command-exists": "^1.2.9", "devtools-protocol": "^0.0.955664", + "dotenv": "^16.0.0", "execa": "^6.1.0", "faye-websocket": "^0.11.4", "finalhandler": "^1.2.0", diff --git a/packages/wrangler/src/cli.ts b/packages/wrangler/src/cli.ts index 43168214a304..e151bc881ef8 100644 --- a/packages/wrangler/src/cli.ts +++ b/packages/wrangler/src/cli.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; // Grab locally specified env params from a `.env` file. import process from "process"; import { hideBin } from "yargs/helpers"; import { FatalError } from "./errors";