Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
72946da
Implement .env support in local development
petebacondarwin May 19, 2025
b659ce8
add support to Wrangler for setting the path to the .env files
petebacondarwin Jul 11, 2025
e4a1dba
Add support for loading local vars from .env files to the vite-plugin
petebacondarwin Jul 14, 2025
6bb75c5
add various e2e and fixture tests for dotenv support
petebacondarwin Jul 15, 2025
9148765
add a changeset
petebacondarwin Jul 15, 2025
ffc0922
fixup! add support to Wrangler for setting the path to the .env files
petebacondarwin Jul 18, 2025
35c8831
use `toMatchObject()` in tests
petebacondarwin Jul 18, 2025
76eee90
add support for CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV to opt-out of .…
petebacondarwin Jul 18, 2025
18da8be
tidy up `envFiles` documentation
petebacondarwin Jul 18, 2025
85c487e
fixup! use `toMatchObject()` in tests
petebacondarwin Jul 18, 2025
186d19c
fixup! Add support for loading local vars from .env files to the vite…
petebacondarwin Jul 18, 2025
6cea665
review fixups
petebacondarwin Jul 18, 2025
b363993
fix up snapshots after rebase
petebacondarwin Jul 18, 2025
281b6c4
more fixups
petebacondarwin Jul 18, 2025
863a09e
more fixups
petebacondarwin Jul 18, 2025
6bd0490
simplify boolean env var defaults
petebacondarwin Jul 20, 2025
75ba0d2
review fixups
petebacondarwin Jul 20, 2025
cc130a6
skip buildAndPreview test on Windows
petebacondarwin Jul 21, 2025
9a84592
move config update handling to a devServer watcher rather than hotUpdate
petebacondarwin Jul 25, 2025
a347c5e
don't run playgrounds in parallel
petebacondarwin Jul 28, 2025
4523114
force runs of playground tests
petebacondarwin Jul 28, 2025
1beee4c
log configid
petebacondarwin Jul 28, 2025
b0b9655
remove watchers
petebacondarwin Jul 28, 2025
35cb6bf
wait for miniflare to be ready before setting options
petebacondarwin Jul 28, 2025
bf5581e
dispose vite server between playground tests
petebacondarwin Jul 28, 2025
2a3be8c
More logs on mockfile changes
petebacondarwin Jul 29, 2025
bc34d6a
handle internal body reading errors in miniflare
petebacondarwin Jul 29, 2025
ad20352
test updates
petebacondarwin Jul 29, 2025
f52d4b1
don't run `page.goto()` in the setup file
petebacondarwin Jul 29, 2025
3b62d4b
restart fixes
petebacondarwin Jul 29, 2025
0777d99
dispose of Miniflare instance when Vite is disposed
petebacondarwin Jul 29, 2025
6fd4861
log stack
petebacondarwin Jul 29, 2025
fe5d375
more diagnostics
petebacondarwin Jul 30, 2025
761bdfa
fix lockfile
petebacondarwin Jul 30, 2025
e692a46
separate forks
petebacondarwin Jul 30, 2025
c51ba25
final cleanups
petebacondarwin Jul 30, 2025
c311775
Revert "force runs of playground tests"
petebacondarwin Jul 30, 2025
2a555d4
longer timeout on the container fixture test
petebacondarwin Jul 30, 2025
0f93e5e
correctly unwatch config files
petebacondarwin Jul 30, 2025
faba736
revert interactive dev fix
petebacondarwin Jul 30, 2025
5a4592b
cleanup beforeAll in vitest-setup
petebacondarwin Jul 30, 2025
7ec7a0c
remove redundant change to `restartingServer` flag
petebacondarwin Jul 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/moody-breads-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@cloudflare/vite-plugin": minor
"wrangler": minor
---

Add support for loading local dev vars from .env files

If there are no `.dev.vars` or `.dev.vars.<environment>` files, when running Wrangler or the Vite plugin in local development mode,
they will now try to load additional local dev vars from `.env`, `.env.local`, `.env.<environment>` and `.env.<environment>.local` files.

These loaded vars are only for local development and have no effect in production to the vars in a deployed Worker.
Wrangler and Vite will continue to load `.env` files in order to configure themselves as a tool.

Further details:

- In `vite build` the local vars will be computed and stored in a `.dev.vars` file next to the compiled Worker code, so that `vite preview` can use them.
- The `wrangler types` command will similarly read the `.env` files (if no `.dev.vars` files) in order to generate the `Env` interface.
- If the `CLOUDFLARE_LOAD_DEV_VARS_FROM_DOT_ENV` environment variable is `"false"` then local dev variables will not be loaded from `.env` files.
- If the `CLOUDFLARE_INCLUDE_PROCESS_ENV` environment variable is `"true"` then all the environment variables found on `process.env` will be included as local dev vars.
- Wrangler (but not Vite plugin) also now supports the `--env-file=<path/to/dotenv/file>` global CLI option. This affects both loading `.env` to configure Wrangler the tool as well as loading local dev vars.
1 change: 1 addition & 0 deletions .github/workflows/test-and-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ jobs:
WRANGLER_LOG_PATH: ${{ runner.temp }}/wrangler-debug-logs/
TEST_REPORT_PATH: ${{ runner.temp }}/test-report/index.html
CI_OS: ${{ matrix.description }}
NODE_DEBUG: "@cloudflare:vite-plugin"

- name: Upload turbo logs
if: always()
Expand Down
1 change: 1 addition & 0 deletions fixtures/nodejs-hybrid-app/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEV_VAR_FROM_DOT_ENV="dev-var-from-dot-env"
1 change: 1 addition & 0 deletions fixtures/nodejs-hybrid-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.env
4 changes: 4 additions & 0 deletions fixtures/nodejs-hybrid-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default {
return testGetRandomValues();
case "/test-process":
return testProcessBehavior();
case "/env":
return Response.json(env);
case "/process-env":
return Response.json(process.env);
case "/query":
return testPostgresLibrary(env, ctx);
case "/test-x509-certificate":
Expand Down
18 changes: 18 additions & 0 deletions fixtures/nodejs-hybrid-app/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,22 @@ describe("nodejs compat", () => {
const response = await fetch(`http://${ip}:${port}/test-http`);
await expect(response.text()).resolves.toBe("OK");
});

test("process.env contains vars", async ({ expect }) => {
const { ip, port } = wrangler;
const response = await fetch(`http://${ip}:${port}/process-env`);
await expect(response.json()).resolves.toMatchObject({
DB_HOSTNAME: "hh-pgsql-public.ebi.ac.uk",
DEV_VAR_FROM_DOT_ENV: "dev-var-from-dot-env",
});
});

test("env contains vars", async ({ expect }) => {
const { ip, port } = wrangler;
const response = await fetch(`http://${ip}:${port}/env`);
await expect(response.json()).resolves.toMatchObject({
DB_HOSTNAME: "hh-pgsql-public.ebi.ac.uk",
DEV_VAR_FROM_DOT_ENV: "dev-var-from-dot-env",
});
});
});
5 changes: 3 additions & 2 deletions fixtures/nodejs-hybrid-app/wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
{
"name": "nodejs-hybrid-app",
"main": "src/index.ts",
// Setting compat date to 2024/09/23 means we don't need to use `nodejs_compat_v2`
"compatibility_date": "2024-09-23",
// Setting compat date after 2024/09/23 means we don't need to use `nodejs_compat_v2`
// Setting compat date after 2025/04/01 means we don't need to use `nodejs_compat_populate_process_env`
"compatibility_date": "2025-07-01",
"compatibility_flags": ["nodejs_compat"],
/*
These DB connection values are to a public database containing information about
Expand Down
1 change: 1 addition & 0 deletions fixtures/pages-workerjs-app/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=bar
1 change: 1 addition & 0 deletions fixtures/pages-workerjs-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.env
2 changes: 1 addition & 1 deletion fixtures/pages-workerjs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"sideEffects": false,
"scripts": {
"check:type": "tsc",
"dev": "wrangler pages dev ./workerjs-test --port 8792",
"dev": "wrangler pages dev ./workerjs-test --port 8792 --compatibility-date=2025-07-15",
"test:ci": "vitest run",
"test:watch": "vitest",
"type:tests": "tsc -p ./tests/tsconfig.json"
Expand Down
35 changes: 31 additions & 4 deletions fixtures/pages-workerjs-app/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@ describe("Pages _worker.js", () => {
const { ip, port, stop } = await runWranglerPagesDev(
resolve(__dirname, ".."),
"./workerjs-test",
["--no-bundle=false", "--port=0", "--inspector-port=0"]
[
"--no-bundle=false",
"--port=0",
"--inspector-port=0",
"--compatibility-date=2025-07-15",
]
);
try {
await expect(
Expand All @@ -52,7 +57,12 @@ describe("Pages _worker.js", () => {
const { ip, port, stop } = await runWranglerPagesDev(
resolve(__dirname, ".."),
"./workerjs-test",
["--bundle", "--port=0", "--inspector-port=0"]
[
"--bundle",
"--port=0",
"--inspector-port=0",
"--compatibility-date=2025-07-15",
]
);
try {
await expect(
Expand All @@ -71,8 +81,9 @@ describe("Pages _worker.js", () => {
await runWranglerPagesDev(resolve(__dirname, ".."), "./workerjs-test", [
"--port=0",
"--inspector-port=0",
"--compatibility-date=2025-07-15",
]);
vi.waitFor(
await vi.waitFor(
() => {
expect(getOutput()).toContain("Ready on");
},
Expand Down Expand Up @@ -118,7 +129,7 @@ describe("Pages _worker.js", () => {
"--port=0",
"--inspector-port=0",
]);
vi.waitFor(
await vi.waitFor(
() => {
expect(getOutput()).toContain("Ready on");
},
Expand Down Expand Up @@ -157,6 +168,22 @@ describe("Pages _worker.js", () => {
}
});

// Serendipitously, this .env reading also works for `wrangler pages dev`.
it("should read local dev vars from the .env file", async ({ expect }) => {
const { ip, port, stop } = await runWranglerPagesDev(
resolve(__dirname, ".."),
"./workerjs-test",
["--port=0", "--inspector-port=0", "--compatibility-date=2025-07-15"]
);
try {
const response = await fetch(`http://${ip}:${port}/env`);
const env = (await response.json()) as { FOO: string };
expect(env.FOO).toBe("bar");
} finally {
await stop();
}
});

async function tryRename(
basePath: string,
from: string,
Expand Down
8 changes: 6 additions & 2 deletions fixtures/pages-workerjs-app/workerjs-test/_worker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import text from "./other-script";

export default {
async fetch() {
return new Response(text);
async fetch(request, env) {
if (request.url.endsWith("/env")) {
return Response.json(env);
} else {
return new Response(text);
}
},
};
1 change: 1 addition & 0 deletions fixtures/worker-app/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
FOO=bar
1 change: 1 addition & 0 deletions fixtures/worker-app/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist
!.env
1 change: 1 addition & 0 deletions fixtures/worker-app/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default {
const { pathname, origin, hostname, host } = new URL(request.url);
if (pathname.startsWith("/fav"))
return new Response("Not found", { status: 404 });
if (pathname === "/env") return Response.json(env.FOO);
if (pathname === "/version_metadata") return Response.json(env.METADATA);
if (pathname === "/random") return new Response(hexEncode(randomBytes(8)));
if (pathname === "/error") throw new Error("Oops!");
Expand Down
6 changes: 6 additions & 0 deletions fixtures/worker-app/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,10 @@ describe("'wrangler dev' correctly renders pages", () => {
]
`);
});

it("reads local dev vars from the .env file", async ({ expect }) => {
const response = await fetch(`http://${ip}:${port}/env`);
const env = await response.text();
expect(env).toBe(`"bar"`);
});
});
113 changes: 61 additions & 52 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -776,56 +776,6 @@ export function _transformsForContentEncodingAndContentType(
return encoders;
}

async function writeResponse(response: Response, res: http.ServerResponse) {
// Convert headers into Node-friendly format
const headers: http.OutgoingHttpHeaders = {};
for (const entry of response.headers) {
const key = entry[0].toLowerCase();
const value = entry[1];
if (key === "set-cookie") {
headers[key] = response.headers.getSetCookie();
} else {
headers[key] = value;
}
}

// If a `Content-Encoding` header is set, we'll need to encode the body
// (likely only set by custom service bindings)
const encoding = headers["content-encoding"]?.toString();
const type = headers["content-type"]?.toString();
const encoders = _transformsForContentEncodingAndContentType(encoding, type);
if (encoders.length > 0) {
// `Content-Length` if set, will be wrong as it's for the decoded length
delete headers["content-length"];
}

res.writeHead(response.status, response.statusText, headers);

// `initialStream` is the stream we'll write the response to. It
// should end up as the first encoder, piping to the next encoder,
// and finally piping to the response:
//
// encoders[0] (initialStream) -> encoders[1] -> res
//
// Not using `pipeline(passThrough, ...encoders, res)` here as that
// gives a premature close error with server sent events. This also
// avoids creating an extra stream even when we're not encoding.
let initialStream: Writable = res;
for (let i = encoders.length - 1; i >= 0; i--) {
encoders[i].pipe(initialStream);
initialStream = encoders[i];
}

// Response body may be null if empty
if (response.body) {
for await (const chunk of response.body) {
if (chunk) initialStream.write(chunk);
}
}

initialStream.end();
}

function safeReadableStreamFrom(iterable: AsyncIterable<Uint8Array>) {
// Adapted from `undici`, catches errors from `next()` to avoid unhandled
// rejections from aborted request body streams:
Expand Down Expand Up @@ -1344,7 +1294,7 @@ export class Miniflare {
res.writeHead(404);
res.end();
} else {
await writeResponse(response, res);
await this.#writeResponse(response, res);
}
}

Expand Down Expand Up @@ -1402,7 +1352,7 @@ export class Miniflare {
}

// Otherwise, send the response as is (e.g. unauthorised)
await writeResponse(response, res);
await this.#writeResponse(response, res);
};

#handleLoopbackConnect = async (
Expand Down Expand Up @@ -1508,6 +1458,65 @@ export class Miniflare {
});
};

async #writeResponse(response: Response, res: http.ServerResponse) {
// Convert headers into Node-friendly format
const headers: http.OutgoingHttpHeaders = {};
for (const entry of response.headers) {
const key = entry[0].toLowerCase();
const value = entry[1];
if (key === "set-cookie") {
headers[key] = response.headers.getSetCookie();
} else {
headers[key] = value;
}
}

// If a `Content-Encoding` header is set, we'll need to encode the body
// (likely only set by custom service bindings)
const encoding = headers["content-encoding"]?.toString();
const type = headers["content-type"]?.toString();
const encoders = _transformsForContentEncodingAndContentType(
encoding,
type
);
if (encoders.length > 0) {
// `Content-Length` if set, will be wrong as it's for the decoded length
delete headers["content-length"];
}

res.writeHead(response.status, response.statusText, headers);

// `initialStream` is the stream we'll write the response to. It
// should end up as the first encoder, piping to the next encoder,
// and finally piping to the response:
//
// encoders[0] (initialStream) -> encoders[1] -> res
//
// Not using `pipeline(passThrough, ...encoders, res)` here as that
// gives a premature close error with server sent events. This also
// avoids creating an extra stream even when we're not encoding.
let initialStream: Writable = res;
for (let i = encoders.length - 1; i >= 0; i--) {
encoders[i].pipe(initialStream);
initialStream = encoders[i];
}

// Response body may be null if empty
if (response.body) {
try {
for await (const chunk of response.body) {
if (chunk) initialStream.write(chunk);
}
} catch (error) {
this.#log.debug(
`Error writing response body, closing response early: ${error}`
);
}
}

initialStream.end();
}

async #getLoopbackPort(): Promise<number> {
// This function must be run with `#runtimeMutex` held

Expand Down
Loading
Loading