From c9818cb58188d20ee5cfb566e9f1358a3b65ca8c Mon Sep 17 00:00:00 2001 From: James Opstad <13586373+jamesopstad@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:49:17 +0100 Subject: [PATCH] Load local Worker secrets --- .../vite-plugin-cloudflare/e2e/basic.test.ts | 192 +++++++++--------- .../e2e/fixtures/basic/cloudflare.config.ts | 3 + .../bindings/__tests__/worker.spec.ts | 4 +- .../playground/bindings/cloudflare.config.ts | 4 +- .../playground/bindings/src/index.ts | 24 +-- .../dev-vars/.env.with-specified-env | 1 - .../__tests__/dev-vars-loading.spec.ts | 6 +- .../dev-vars/__tests__/vars-changes.spec.ts | 63 +++--- .../dev-vars-loading.spec.ts | 6 +- .../with-specified-env/vars-changes.spec.ts | 62 +++--- .../vite.config.with-specified-env.ts | 2 +- .../dot-env/.env.with-specified-env | 1 - .../dot-env/__tests__/dot-env-loading.spec.ts | 16 +- .../dot-env/__tests__/vars-changes.spec.ts | 62 +++--- .../dot-env-loading.spec.ts | 6 +- .../with-specified-env/vars-changes.spec.ts | 53 +++-- .../dot-env/vite.config.with-specified-env.ts | 2 +- .../__tests__/importable-env.spec.ts | 20 +- .../process.spec.ts | 3 +- ...ite.config.worker-process-populated-env.ts | 6 +- .../__tests__/build-output-preview.spec.ts | 18 +- .../src/__tests__/dev-vars.spec.ts | 143 ------------- .../src/__tests__/local-env.spec.ts | 41 ++++ .../__tests__/resolve-plugin-config.spec.ts | 167 +++++++++++---- .../src/build-output-preview.ts | 26 ++- .../vite-plugin-cloudflare/src/context.ts | 2 +- .../vite-plugin-cloudflare/src/dev-vars.ts | 87 -------- .../vite-plugin-cloudflare/src/local-env.ts | 54 +++++ .../src/miniflare-options.ts | 86 +++++++- .../src/plugin-config.ts | 56 +++-- .../src/plugins/config.ts | 13 +- .../tsconfig.plugin.json | 5 +- 32 files changed, 639 insertions(+), 595 deletions(-) delete mode 100644 packages/vite-plugin-cloudflare/playground/dev-vars/.env.with-specified-env delete mode 100644 packages/vite-plugin-cloudflare/playground/dot-env/.env.with-specified-env delete mode 100644 packages/vite-plugin-cloudflare/src/__tests__/dev-vars.spec.ts create mode 100644 packages/vite-plugin-cloudflare/src/__tests__/local-env.spec.ts delete mode 100644 packages/vite-plugin-cloudflare/src/dev-vars.ts create mode 100644 packages/vite-plugin-cloudflare/src/local-env.ts diff --git a/packages/vite-plugin-cloudflare/e2e/basic.test.ts b/packages/vite-plugin-cloudflare/e2e/basic.test.ts index 73dfd65ed6e..0574b4727e6 100644 --- a/packages/vite-plugin-cloudflare/e2e/basic.test.ts +++ b/packages/vite-plugin-cloudflare/e2e/basic.test.ts @@ -74,106 +74,112 @@ describe("basic e2e tests", () => { } ); - // TODO: Reinstate when local Worker secrets are loaded with - // cloudflare.config.ts. - describe.skip("environment variables", () => { - test("can read vars from cloudflare.config.ts and .env", async ({ - expect, - }) => { - await writeFile( - projectPath + "/.env", - "SECRET_A=dev-1\nSECRET_B=dev-2" - ); - onTestFinished(async () => { - await rm(projectPath + "/.env"); - }); - const proc = await runLongLived(pm, command, projectPath); - const url = await waitForReady(proc); - expect(await fetchJson(url + "/env/")).toMatchObject({ - SECRET_A: "dev-1", - SECRET_B: "dev-2", - VAR_1: "var-1", + describe.skipIf(isBuildAndPreviewOnWindows(command))( + "environment variables", + () => { + test("can read vars from cloudflare.config.ts and .env", async ({ + expect, + }) => { + await writeFile( + projectPath + "/.env", + "SECRET_A=dev-1\nSECRET_B=dev-2" + ); + onTestFinished(async () => { + await rm(projectPath + "/.env"); + }); + const proc = await runLongLived(pm, command, projectPath); + const url = await waitForReady(proc); + expect(await fetchJson(url + "/env/")).toMatchObject({ + SECRET_A: "dev-1", + SECRET_B: "dev-2", + VAR_1: "var-1", + }); }); - }); - test("will not load local dev vars from .env if there is a .dev.vars file", async ({ - expect, - }) => { - await writeFile( - projectPath + "/.env", - "SECRET_A=dot-env-1\nSECRET_B=dot-env-2" - ); - await writeFile( - projectPath + "/.dev.vars", - "SECRET_A=dev-dot-vars-1" - ); - onTestFinished(async () => { - await rm(projectPath + "/.env"); - await rm(projectPath + "/.dev.vars"); + test("will not load local dev vars from .env if there is a .dev.vars file", async ({ + expect, + }) => { + await writeFile( + projectPath + "/.env", + "SECRET_A=dot-env-1\nSECRET_B=dot-env-2" + ); + await writeFile( + projectPath + "/.dev.vars", + "SECRET_A=dev-dot-vars-1" + ); + onTestFinished(async () => { + await rm(projectPath + "/.env"); + await rm(projectPath + "/.dev.vars"); + }); + const proc = await runLongLived(pm, command, projectPath); + const url = await waitForReady(proc); + expect(await fetchJson(url + "/env/")).toMatchObject({ + SECRET_A: "dev-dot-vars-1", + VAR_1: "var-1", + }); }); - const proc = await runLongLived(pm, command, projectPath); - const url = await waitForReady(proc); - expect(await fetchJson(url + "/env/")).toMatchObject({ - SECRET_A: "dev-dot-vars-1", - VAR_1: "var-1", - }); - }); - test("can merge vars from cloudflare.config.ts, .env, and .env.local", async ({ - expect, - }) => { - await writeFile( - projectPath + "/.env", - "SECRET_A=dev-1\nSECRET_B=dev-2" - ); - await writeFile(projectPath + "/.env.local", "SECRET_A=local-dev-1"); - onTestFinished(async () => { - await rm(projectPath + "/.env"); - await rm(projectPath + "/.env.local"); + test("can merge vars from cloudflare.config.ts, .env, and .env.local", async ({ + expect, + }) => { + await writeFile( + projectPath + "/.env", + "SECRET_A=dev-1\nSECRET_B=dev-2" + ); + await writeFile( + projectPath + "/.env.local", + "SECRET_A=local-dev-1" + ); + onTestFinished(async () => { + await rm(projectPath + "/.env"); + await rm(projectPath + "/.env.local"); + }); + const proc = await runLongLived(pm, command, projectPath); + const url = await waitForReady(proc); + expect(await fetchJson(url + "/env/")).toMatchObject({ + SECRET_A: "local-dev-1", + SECRET_B: "dev-2", + VAR_1: "var-1", + }); }); - const proc = await runLongLived(pm, command, projectPath); - const url = await waitForReady(proc); - expect(await fetchJson(url + "/env/")).toMatchObject({ - SECRET_A: "local-dev-1", - SECRET_B: "dev-2", - VAR_1: "var-1", - }); - }); - test("can merge vars from cloudflare.config.ts, .env, .env.local, and environment specific files", async ({ - expect, - }) => { - await writeFile( - projectPath + "/.env", - "SECRET_A=dev-1\nSECRET_B=dev-2" - ); - await writeFile(projectPath + "/.env.local", "SECRET_A=local-dev-1"); - await writeFile( - projectPath + "/.env.staging", - "SECRET_B=staging-2\nSECRET_C=staging-3" - ); - await writeFile( - projectPath + "/.env.staging.local", - "SECRET_C=local-staging-3" - ); - onTestFinished(async () => { - await rm(projectPath + "/.env"); - await rm(projectPath + "/.env.local"); - await rm(projectPath + "/.env.staging"); - await rm(projectPath + "/.env.staging.local"); + test("can merge vars from cloudflare.config.ts, .env, .env.local, and mode-specific files", async ({ + expect, + }) => { + const mode = command === "dev" ? "development" : "production"; + await writeFile( + projectPath + "/.env", + "SECRET_A=dev-1\nSECRET_B=dev-2" + ); + await writeFile( + projectPath + "/.env.local", + "SECRET_A=local-dev-1" + ); + await writeFile( + projectPath + `/.env.${mode}`, + "SECRET_B=staging-2\nSECRET_C=staging-3" + ); + await writeFile( + projectPath + `/.env.${mode}.local`, + "SECRET_C=local-staging-3" + ); + onTestFinished(async () => { + await rm(projectPath + "/.env"); + await rm(projectPath + "/.env.local"); + await rm(projectPath + `/.env.${mode}`); + await rm(projectPath + `/.env.${mode}.local`); + }); + const proc = await runLongLived(pm, command, projectPath); + const url = await waitForReady(proc); + expect(await fetchJson(url + "/env/")).toMatchObject({ + SECRET_A: "local-dev-1", + SECRET_B: "staging-2", + SECRET_C: "local-staging-3", + VAR_1: "var-1", + }); }); - const proc = await runLongLived(pm, command, projectPath, { - CLOUDFLARE_ENV: "staging", - }); - const url = await waitForReady(proc); - expect(await fetchJson(url + "/env/")).toMatchObject({ - SECRET_A: "local-dev-1", - SECRET_B: "staging-2", - SECRET_C: "local-staging-3", - VAR_1: "var-1", - }); - }); - }); + } + ); }); }); }); diff --git a/packages/vite-plugin-cloudflare/e2e/fixtures/basic/cloudflare.config.ts b/packages/vite-plugin-cloudflare/e2e/fixtures/basic/cloudflare.config.ts index 6023b8d9158..f36899462dc 100644 --- a/packages/vite-plugin-cloudflare/e2e/fixtures/basic/cloudflare.config.ts +++ b/packages/vite-plugin-cloudflare/e2e/fixtures/basic/cloudflare.config.ts @@ -11,6 +11,9 @@ export default defineWorker({ assets: { notFoundHandling: "single-page-application" }, env: { ASSETS: bindings.assets(), + SECRET_A: bindings.secret(), + SECRET_B: bindings.secret(), + SECRET_C: bindings.secret(), VAR_1: bindings.text("var-1"), }, }); diff --git a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts index 6e85d3b0418..1bad189a8a5 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts @@ -26,9 +26,7 @@ test("ratelimit support", async ({ expect }) => { expect(response).toBe("Rate limit binding works: first: true, second: false"); }); -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("hyperdrive support", async ({ expect }) => { +test("hyperdrive support", async ({ expect }) => { const response = await getTextResponse("/hyperdrive"); expect(response).toBe("Hyperdrive binding works"); }); diff --git a/packages/vite-plugin-cloudflare/playground/bindings/cloudflare.config.ts b/packages/vite-plugin-cloudflare/playground/bindings/cloudflare.config.ts index 7baa566d05b..6ae9e8f8104 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/cloudflare.config.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/cloudflare.config.ts @@ -21,9 +21,7 @@ export default defineWorker({ }), IMAGES: bindings.images(), WAE: bindings.analyticsEngineDataset({ name: "test" }), - // TODO: Reinstate when .env and .dev.vars files are supported with - // cloudflare.config.ts. - // HYPERDRIVE: bindings.hyperdrive({ id: "test-hyperdrive-id" }), + HYPERDRIVE: bindings.hyperdrive({ id: "test-hyperdrive-id" }), RATE_LIMITER: bindings.rateLimit({ namespace: "1001", simple: { limit: 1, period: 60 }, diff --git a/packages/vite-plugin-cloudflare/playground/bindings/src/index.ts b/packages/vite-plugin-cloudflare/playground/bindings/src/index.ts index 90fb23bbe3d..93ab4fa2f36 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/src/index.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/src/index.ts @@ -66,20 +66,18 @@ export default { } ); } - // TODO: Reinstate when .env and .dev.vars files are supported with - // cloudflare.config.ts. - // case "/hyperdrive": { - // if ( - // typeof env.HYPERDRIVE.connect !== "function" || - // typeof env.HYPERDRIVE.connectionString !== "string" - // ) { - // return new Response("Hyperdrive binding is not configured properly", { - // status: 500, - // }); - // } + case "/hyperdrive": { + if ( + typeof env.HYPERDRIVE.connect !== "function" || + typeof env.HYPERDRIVE.connectionString !== "string" + ) { + return new Response("Hyperdrive binding is not configured properly", { + status: 500, + }); + } - // return new Response("Hyperdrive binding works"); - // } + return new Response("Hyperdrive binding works"); + } } return new Response("Please specify a binding you want to test", { diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/.env.with-specified-env b/packages/vite-plugin-cloudflare/playground/dev-vars/.env.with-specified-env deleted file mode 100644 index 69498a43858..00000000000 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/.env.with-specified-env +++ /dev/null @@ -1 +0,0 @@ -CLOUDFLARE_ENV=staging diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/dev-vars-loading.spec.ts b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/dev-vars-loading.spec.ts index a52e07c5838..cfd3994ff4c 100644 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/dev-vars-loading.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/dev-vars-loading.spec.ts @@ -1,11 +1,7 @@ import { test, vi } from "vitest"; import { getJsonResponse, WAIT_FOR_OPTIONS } from "../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("reading variables from a standard .dev.vars file", async ({ - expect, -}) => { +test("reads Worker secrets from .dev.vars", async ({ expect }) => { await vi.waitFor( async () => expect(await getJsonResponse()).toEqual({ diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/vars-changes.spec.ts b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/vars-changes.spec.ts index 9a1b09f29e7..9d10b4a5919 100644 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/vars-changes.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/vars-changes.spec.ts @@ -2,39 +2,42 @@ import * as path from "node:path"; import { test, vi } from "vitest"; import { getJsonResponse, + isBuild, mockFileChange, WAIT_FOR_OPTIONS, } from "../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("successfully updates when a var is updated in a .dev.vars file", async ({ - expect, -}) => { - await vi.waitFor( - async () => - expect(await getJsonResponse()).toEqual({ - "variables present in .dev.vars": { - MY_DEV_VAR_A: "my .dev.vars variable A", - MY_DEV_VAR_B: "my .dev.vars variable B", - MY_DEV_VAR_C: "my .dev.vars variable C", - }, - }), - WAIT_FOR_OPTIONS - ); +test.runIf(!isBuild)( + "reloads Worker secrets when .dev.vars changes", + async ({ expect }) => { + await vi.waitFor( + async () => + expect(await getJsonResponse()).toEqual({ + "variables present in .dev.vars": { + MY_DEV_VAR_A: "my .dev.vars variable A", + MY_DEV_VAR_B: "my .dev.vars variable B", + MY_DEV_VAR_C: "my .dev.vars variable C", + }, + }), + WAIT_FOR_OPTIONS + ); - mockFileChange(path.join(__dirname, "../.dev.vars"), (content) => - content.replace(/my \.dev\.vars variable/g, "my .dev.vars UPDATED variable") - ); + mockFileChange(path.join(__dirname, "../.dev.vars"), (content) => + content.replace( + /my \.dev\.vars variable/g, + "my .dev.vars UPDATED variable" + ) + ); - await vi.waitFor(async () => { - const updatedResponse = await getJsonResponse(); - expect(updatedResponse).toEqual({ - "variables present in .dev.vars": { - MY_DEV_VAR_A: "my .dev.vars UPDATED variable A", - MY_DEV_VAR_B: "my .dev.vars UPDATED variable B", - MY_DEV_VAR_C: "my .dev.vars UPDATED variable C", - }, - }); - }, WAIT_FOR_OPTIONS); -}); + await vi.waitFor(async () => { + const updatedResponse = await getJsonResponse(); + expect(updatedResponse).toEqual({ + "variables present in .dev.vars": { + MY_DEV_VAR_A: "my .dev.vars UPDATED variable A", + MY_DEV_VAR_B: "my .dev.vars UPDATED variable B", + MY_DEV_VAR_C: "my .dev.vars UPDATED variable C", + }, + }); + }, WAIT_FOR_OPTIONS); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/dev-vars-loading.spec.ts b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/dev-vars-loading.spec.ts index a16ce832e28..e0e681860bd 100644 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/dev-vars-loading.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/dev-vars-loading.spec.ts @@ -1,11 +1,7 @@ import { test, vi } from "vitest"; import { getJsonResponse, WAIT_FOR_OPTIONS } from "../../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("reading variables from a staging .dev.vars file", async ({ - expect, -}) => { +test("uses .dev.vars.staging exclusively", async ({ expect }) => { await vi.waitFor( async () => expect(await getJsonResponse()).toEqual({ diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/vars-changes.spec.ts b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/vars-changes.spec.ts index 43ab5af087f..132c17a31b8 100644 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/vars-changes.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dev-vars/__tests__/with-specified-env/vars-changes.spec.ts @@ -2,40 +2,40 @@ import * as path from "node:path"; import { test, vi } from "vitest"; import { getJsonResponse, + isBuild, mockFileChange, WAIT_FOR_OPTIONS, } from "../../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("successfully updates when a var is updated in a .dev.vars.staging file", async ({ - expect, -}) => { - await vi.waitFor( - async () => - expect(await getJsonResponse()).toEqual({ - "variables present in .dev.vars.staging": { - MY_DEV_VAR_A: "my .dev.vars staging variable A", - MY_DEV_VAR_B: "my .dev.vars staging variable B", - }, - }), - WAIT_FOR_OPTIONS - ); +test.runIf(!isBuild)( + "reloads Worker secrets when .dev.vars.staging changes", + async ({ expect }) => { + await vi.waitFor( + async () => + expect(await getJsonResponse()).toEqual({ + "variables present in .dev.vars.staging": { + MY_DEV_VAR_A: "my .dev.vars staging variable A", + MY_DEV_VAR_B: "my .dev.vars staging variable B", + }, + }), + WAIT_FOR_OPTIONS + ); - mockFileChange(path.join(__dirname, "../../.dev.vars.staging"), (content) => - content.replace( - /my \.dev\.vars staging variable/g, - "my .dev.vars UPDATED staging variable" - ) - ); + mockFileChange(path.join(__dirname, "../../.dev.vars.staging"), (content) => + content.replace( + /my \.dev\.vars staging variable/g, + "my .dev.vars UPDATED staging variable" + ) + ); - await vi.waitFor(async () => { - const updatedResponse = await getJsonResponse(); - expect(updatedResponse).toEqual({ - "variables present in .dev.vars.staging": { - MY_DEV_VAR_A: "my .dev.vars UPDATED staging variable A", - MY_DEV_VAR_B: "my .dev.vars UPDATED staging variable B", - }, - }); - }, WAIT_FOR_OPTIONS); -}); + await vi.waitFor(async () => { + const updatedResponse = await getJsonResponse(); + expect(updatedResponse).toEqual({ + "variables present in .dev.vars.staging": { + MY_DEV_VAR_A: "my .dev.vars UPDATED staging variable A", + MY_DEV_VAR_B: "my .dev.vars UPDATED staging variable B", + }, + }); + }, WAIT_FOR_OPTIONS); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.with-specified-env.ts b/packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.with-specified-env.ts index 2d633dcf761..456f7f9e676 100644 --- a/packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.with-specified-env.ts +++ b/packages/vite-plugin-cloudflare/playground/dev-vars/vite.config.with-specified-env.ts @@ -2,7 +2,7 @@ import { cloudflare } from "@cloudflare/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ - mode: "with-specified-env", + mode: "staging", plugins: [ cloudflare({ types: { includeRuntime: false }, diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/.env.with-specified-env b/packages/vite-plugin-cloudflare/playground/dot-env/.env.with-specified-env deleted file mode 100644 index 69498a43858..00000000000 --- a/packages/vite-plugin-cloudflare/playground/dot-env/.env.with-specified-env +++ /dev/null @@ -1 +0,0 @@ -CLOUDFLARE_ENV=staging diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/dot-env-loading.spec.ts b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/dot-env-loading.spec.ts index 2b05879d277..ce8c4330668 100644 --- a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/dot-env-loading.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/dot-env-loading.spec.ts @@ -1,16 +1,12 @@ import { test } from "vitest"; import { getJsonResponse } from "../../__test-utils__"; -const expectedVars = { - MY_DEV_VAR_A: "my .env variable A", - MY_DEV_VAR_B: "my .env variable B", - MY_DEV_VAR_C: "my .env variable C", -}; - -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("reading variables from a standard .env file", async ({ expect }) => { +test("reads Worker secrets from .env", async ({ expect }) => { expect(await getJsonResponse()).toEqual({ - "variables loaded from .env": expectedVars, + "variables loaded from .env": { + MY_DEV_VAR_A: "my .env variable A", + MY_DEV_VAR_B: "my .env variable B", + MY_DEV_VAR_C: "my .env variable C", + }, }); }); diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/vars-changes.spec.ts b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/vars-changes.spec.ts index 0844e650242..8cc4c6b9a50 100644 --- a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/vars-changes.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/vars-changes.spec.ts @@ -2,40 +2,40 @@ import * as path from "node:path"; import { test, vi } from "vitest"; import { getJsonResponse, + isBuild, mockFileChange, WAIT_FOR_OPTIONS, } from "../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("successfully updates when a var is updated in a .env file", async ({ - expect, -}) => { - await vi.waitFor( - async () => - expect(await getJsonResponse()).toEqual({ - "variables loaded from .env": { - MY_DEV_VAR_A: "my .env variable A", - MY_DEV_VAR_B: "my .env variable B", - MY_DEV_VAR_C: "my .env variable C", - }, - }), - WAIT_FOR_OPTIONS - ); +test.runIf(!isBuild)( + "reloads Worker secrets when .env changes", + async ({ expect }) => { + await vi.waitFor( + async () => + expect(await getJsonResponse()).toEqual({ + "variables loaded from .env": { + MY_DEV_VAR_A: "my .env variable A", + MY_DEV_VAR_B: "my .env variable B", + MY_DEV_VAR_C: "my .env variable C", + }, + }), + WAIT_FOR_OPTIONS + ); - mockFileChange(path.join(__dirname, "../.env"), (content) => - content.replace(/my \.env/g, "my .env UPDATED") - ); + mockFileChange(path.join(__dirname, "../.env"), (content) => + content.replace(/my \.env/g, "my .env UPDATED") + ); - await vi.waitFor( - async () => - expect(await getJsonResponse()).toEqual({ - "variables loaded from .env": { - MY_DEV_VAR_A: "my .env UPDATED variable A", - MY_DEV_VAR_B: "my .env UPDATED variable B", - MY_DEV_VAR_C: "my .env UPDATED variable C", // Note that unlike .dev.vars, we merge .env files - }, - }), - WAIT_FOR_OPTIONS - ); -}); + await vi.waitFor( + async () => + expect(await getJsonResponse()).toEqual({ + "variables loaded from .env": { + MY_DEV_VAR_A: "my .env UPDATED variable A", + MY_DEV_VAR_B: "my .env UPDATED variable B", + MY_DEV_VAR_C: "my .env UPDATED variable C", + }, + }), + WAIT_FOR_OPTIONS + ); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/dot-env-loading.spec.ts b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/dot-env-loading.spec.ts index 97e0ae1e8bb..07dac293ab3 100644 --- a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/dot-env-loading.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/dot-env-loading.spec.ts @@ -1,14 +1,12 @@ import { test } from "vitest"; import { getJsonResponse } from "../../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("reading variables from a staging .env file", async ({ expect }) => { +test("merges Worker secrets from .env and .env.staging", async ({ expect }) => { expect(await getJsonResponse()).toEqual({ "variables loaded from .env and .env.staging": { MY_DEV_VAR_A: "my .env staging variable A", MY_DEV_VAR_B: "my .env staging variable B", - MY_DEV_VAR_C: "my .env variable C", // Note that unlike .dev.vars, we merge .env files + MY_DEV_VAR_C: "my .env variable C", }, }); }); diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/vars-changes.spec.ts b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/vars-changes.spec.ts index c52849408c0..b5af66ff1e1 100644 --- a/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/vars-changes.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/dot-env/__tests__/with-specified-env/vars-changes.spec.ts @@ -2,37 +2,36 @@ import * as path from "node:path"; import { test, vi } from "vitest"; import { getJsonResponse, + isBuild, mockFileChange, WAIT_FOR_OPTIONS, } from "../../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("successfully updates when a var is updated in a .env.staging file", async ({ - expect, -}) => { - const originalResponseContent = { - "variables loaded from .env and .env.staging": { - MY_DEV_VAR_A: "my .env staging variable A", - MY_DEV_VAR_B: "my .env staging variable B", - MY_DEV_VAR_C: "my .env variable C", // Note that unlike .dev.vars, we merge .env files - }, - }; - const originalResponse = await getJsonResponse(); - expect(originalResponse).toEqual(originalResponseContent); - - mockFileChange(path.join(__dirname, "../../.env.staging"), (content) => - content.replace(/my \.env staging/g, "my .env UPDATED staging") - ); - - await vi.waitFor(async () => { - const updatedResponse = await getJsonResponse(); - expect(updatedResponse).toEqual({ +test.runIf(!isBuild)( + "reloads Worker secrets when .env.staging changes", + async ({ expect }) => { + const originalResponse = await getJsonResponse(); + expect(originalResponse).toEqual({ "variables loaded from .env and .env.staging": { - MY_DEV_VAR_A: "my .env UPDATED staging variable A", - MY_DEV_VAR_B: "my .env UPDATED staging variable B", - MY_DEV_VAR_C: "my .env variable C", // Note that unlike .dev.vars, we merge .env files + MY_DEV_VAR_A: "my .env staging variable A", + MY_DEV_VAR_B: "my .env staging variable B", + MY_DEV_VAR_C: "my .env variable C", }, }); - }, WAIT_FOR_OPTIONS); -}); + + mockFileChange(path.join(__dirname, "../../.env.staging"), (content) => + content.replace(/my \.env staging/g, "my .env UPDATED staging") + ); + + await vi.waitFor(async () => { + const updatedResponse = await getJsonResponse(); + expect(updatedResponse).toEqual({ + "variables loaded from .env and .env.staging": { + MY_DEV_VAR_A: "my .env UPDATED staging variable A", + MY_DEV_VAR_B: "my .env UPDATED staging variable B", + MY_DEV_VAR_C: "my .env variable C", + }, + }); + }, WAIT_FOR_OPTIONS); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/dot-env/vite.config.with-specified-env.ts b/packages/vite-plugin-cloudflare/playground/dot-env/vite.config.with-specified-env.ts index 2d633dcf761..456f7f9e676 100644 --- a/packages/vite-plugin-cloudflare/playground/dot-env/vite.config.with-specified-env.ts +++ b/packages/vite-plugin-cloudflare/playground/dot-env/vite.config.with-specified-env.ts @@ -2,7 +2,7 @@ import { cloudflare } from "@cloudflare/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ - mode: "with-specified-env", + mode: "staging", plugins: [ cloudflare({ types: { includeRuntime: false }, diff --git a/packages/vite-plugin-cloudflare/playground/importable-env/__tests__/importable-env.spec.ts b/packages/vite-plugin-cloudflare/playground/importable-env/__tests__/importable-env.spec.ts index b8a37d20c71..0fabeeebc4a 100644 --- a/packages/vite-plugin-cloudflare/playground/importable-env/__tests__/importable-env.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/importable-env/__tests__/importable-env.spec.ts @@ -1,9 +1,7 @@ import { test } from "vitest"; import { getJsonResponse, serverLogs } from "../../__test-utils__"; -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("the importable env is accessible from outside the request handler", async ({ +test("the importable env is accessible outside the request handler", ({ expect, }) => { expect(serverLogs.info.join()).toMatch( @@ -11,11 +9,9 @@ test.skip("the importable env is accessible from outside the request handler", a ); }); -test.skip("the fetch handler env contains the correct entries", async ({ - expect, -}) => { +test("the fetch handler env contains the local secret", async ({ expect }) => { const json = (await getJsonResponse()) as Record; - expect(json?.["entries of the fetch handler env"]).toEqual([ + expect(json["entries of the fetch handler env"]).toEqual([ { key: "importable-env_VAR", value: "my importable env variable", @@ -27,11 +23,9 @@ test.skip("the fetch handler env contains the correct entries", async ({ ]); }); -test.skip("the imported env contains the correct entries", async ({ - expect, -}) => { +test("the imported env contains the local secret", async ({ expect }) => { const json = (await getJsonResponse()) as Record; - expect(json?.["entries of the imported env"]).toEqual([ + expect(json["entries of the imported env"]).toEqual([ { key: "importable-env_VAR", value: "my importable env variable", @@ -43,9 +37,9 @@ test.skip("the imported env contains the correct entries", async ({ ]); }); -test.skip("the entries in the fetch handler env and the imported env are the same", async ({ +test("the handler and imported env contain the same entries", async ({ expect, }) => { const json = (await getJsonResponse()) as Record; - expect(json?.["are the two set of entries the same?"]).toEqual(true); + expect(json["are the two set of entries the same?"]).toBe(true); }); diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process-populated-env/process.spec.ts b/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process-populated-env/process.spec.ts index da9c3e5ac73..dec4beb4a19 100644 --- a/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process-populated-env/process.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/node-compat/__tests__/worker-process-populated-env/process.spec.ts @@ -1,8 +1,7 @@ import { test, vi } from "vitest"; import { getTextResponse, WAIT_FOR_OPTIONS } from "../../../__test-utils__"; -// TODO: Reinstate when local value files can be resolved relative to the configured Worker's entrypoint. -test.skip("should get a populated process.env object", async ({ expect }) => { +test("should get a populated process.env object", async ({ expect }) => { await vi.waitFor( async () => expect(await getTextResponse()).toBe(`OK!`), WAIT_FOR_OPTIONS diff --git a/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process-populated-env.ts b/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process-populated-env.ts index 857175f2db7..e7c8b59007a 100644 --- a/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process-populated-env.ts +++ b/packages/vite-plugin-cloudflare/playground/node-compat/vite.config.worker-process-populated-env.ts @@ -2,6 +2,7 @@ import { cloudflare } from "@cloudflare/vite-plugin"; import { defineConfig } from "vite"; export default defineConfig({ + envDir: "worker-process-populated-env", build: { outDir: "dist/worker-process-populated-env", }, @@ -16,7 +17,10 @@ export default defineConfig({ "nodejs_compat", "nodejs_compat_populate_process_env", ], - env: { FOO: { type: "text", value: "foo value" } }, + env: { + FOO: { type: "text", value: "foo value" }, + BAR: { type: "secret" }, + }, }, inspectorPort: false, persistState: false, diff --git a/packages/vite-plugin-cloudflare/src/__tests__/build-output-preview.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/build-output-preview.spec.ts index 47fecf2eade..998bc654061 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/build-output-preview.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/build-output-preview.spec.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, test, vi } from "vitest"; -import { readBuildOutputWorkers } from "../build-output-preview"; +import { readBuildOutputPreview } from "../build-output-preview"; import type { BuildOutputWorker } from "@cloudflare/build-output-utils"; const { readBuildOutputMock } = vi.hoisted(() => ({ @@ -24,12 +24,12 @@ function createWorker(name: string): BuildOutputWorker { }; } -describe("readBuildOutputWorkers", () => { +describe("readBuildOutputPreview", () => { beforeEach(() => { readBuildOutputMock.mockReset(); }); - test("selects the default and auxiliary Workers for preview", async ({ + test("selects the default and auxiliary Workers for ordinary preview", async ({ expect, }) => { readBuildOutputMock.mockResolvedValue({ @@ -41,9 +41,9 @@ describe("readBuildOutputWorkers", () => { }, }); - const result = await readBuildOutputWorkers("/project", false); + const result = await readBuildOutputPreview("/project", false); - expect(result.map((worker) => worker.config.name)).toEqual([ + expect(result.workers.map((worker) => worker.config.name)).toEqual([ "entry-worker", "auxiliary-worker", ]); @@ -61,9 +61,9 @@ describe("readBuildOutputWorkers", () => { }, }); - const result = await readBuildOutputWorkers("/project", true); + const result = await readBuildOutputPreview("/project", true); - expect(result.map((worker) => worker.config.name)).toEqual([ + expect(result.workers.map((worker) => worker.config.name)).toEqual([ "prerender-worker", "auxiliary-worker", ]); @@ -80,9 +80,9 @@ describe("readBuildOutputWorkers", () => { }, }); - const result = await readBuildOutputWorkers("/project", true); + const result = await readBuildOutputPreview("/project", true); - expect(result.map((worker) => worker.config.name)).toEqual([ + expect(result.workers.map((worker) => worker.config.name)).toEqual([ "entry-worker", "auxiliary-worker", ]); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/dev-vars.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/dev-vars.spec.ts deleted file mode 100644 index 5dea2d45a15..00000000000 --- a/packages/vite-plugin-cloudflare/src/__tests__/dev-vars.spec.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { test } from "vitest"; - -// TODO: Reinstate when .env and .dev.vars files are supported with -// cloudflare.config.ts. -test.skip("local Worker variables", () => {}); - -// The previous tests are retained below for reinstatement. - -// import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; -// import { tmpdir } from "node:os"; -// import * as path from "node:path"; -// import { removeDirSync } from "@cloudflare/workers-utils"; -// import { afterEach, beforeEach, describe, test } from "vitest"; -// import * as wrangler from "wrangler"; -// import { getLocalDevVarsForPreview, quoteForDotenv } from "../dev-vars"; -// -// /** -// * The values produced by `quoteForDotenv` end up in `dist//.dev.vars` -// * and are read back at preview time by wrangler. These tests pin the -// * round-trip through wrangler's own loader so the contract — not just -// * dotenv's specific behaviour — is what's verified. -// */ -// describe("quoteForDotenv", () => { -// let dir: string; -// -// beforeEach(() => { -// dir = mkdtempSync(path.join(tmpdir(), "vite-plugin-dev-vars-")); -// writeFileSync(path.join(dir, "wrangler.json"), "{}"); -// }); -// afterEach(() => removeDirSync(dir)); -// -// function roundTrip(value: string): string | undefined { -// writeFileSync( -// path.join(dir, ".dev.vars"), -// `KEY=${quoteForDotenv(value)}\n` -// ); -// const vars = wrangler.unstable_getVarsForDev( -// path.join(dir, "wrangler.json"), -// undefined, -// {}, -// undefined, -// true -// ); -// const binding = vars.KEY; -// return binding && "value" in binding && typeof binding.value === "string" -// ? binding.value -// : undefined; -// } -// -// const cases: { name: string; input: string }[] = [ -// { name: "plain string", input: "hello" }, -// { name: "string with spaces", input: "hello world" }, -// { name: "leading and trailing whitespace", input: " spaces " }, -// { name: "empty string", input: "" }, -// { -// name: "JSON-encoded JWK (contains double quotes)", -// input: '{"kty":"OKP","crv":"Ed25519","x":"abc","d":"xyz"}', -// }, -// { name: "value with single quote", input: "it's complicated" }, -// { -// name: "value with both single and double quotes", -// input: `she said "it's fine"`, -// }, -// { -// name: "value with single quote and backslash", -// input: "it's C:\\path\\to\\file", -// }, -// { -// name: "value with single quote and literal \\n", -// input: String.raw`it's \n literal`, -// }, -// { name: "value with backslashes", input: "C:\\path\\to\\file" }, -// { name: "value that looks like ${expansion}", input: "${NOT_REPLACED}" }, -// { name: "value with hash", input: "abc # not a comment" }, -// { name: "value with actual newline", input: "line1\nline2" }, -// ]; -// -// for (const { name, input } of cases) { -// test(`round-trips: ${name}`, ({ expect }) => { -// expect(roundTrip(input)).toBe(input); -// }); -// } -// -// test("throws when a value cannot be losslessly serialized", ({ expect }) => { -// // Contains all three quote characters — no safe encoding under dotenv. -// expect(() => quoteForDotenv("'`\"")).toThrow( -// /Unable to serialize value to \.dev\.vars/ -// ); -// }); -// }); -// -// /** -// * Integration test: prove `getLocalDevVarsForPreview` actually wires -// * `quoteForDotenv` into the path that ends up at `dist//.dev.vars`, -// * so the value a user wrote in their project's `.dev.vars` round-trips -// * through to the preview server. -// */ -// describe("getLocalDevVarsForPreview", () => { -// let dir: string; -// -// beforeEach(() => { -// dir = mkdtempSync(path.join(tmpdir(), "vite-plugin-dev-vars-int-")); -// }); -// afterEach(() => removeDirSync(dir)); -// -// test("re-emits a .dev.vars value containing quotes losslessly", ({ -// expect, -// }) => { -// const inputDir = path.join(dir, "input"); -// const outputDir = path.join(dir, "output"); -// mkdirSync(inputDir); -// mkdirSync(outputDir); -// -// // A user's hand-written .dev.vars. Single-quoted form is read literally -// // by dotenv, so this is the canonical way to embed a JSON value. -// const value = '{"jwk":"contains \\"quotes\\""}'; -// writeFileSync(path.join(inputDir, "wrangler.json"), "{}"); -// writeFileSync(path.join(inputDir, ".dev.vars"), `KEY='${value}'\n`); -// -// const config = wrangler.unstable_readConfig({ -// config: path.join(inputDir, "wrangler.json"), -// }); -// const serialized = getLocalDevVarsForPreview(config, undefined); -// expect(serialized).toBeDefined(); -// -// writeFileSync(path.join(outputDir, "wrangler.json"), "{}"); -// writeFileSync(path.join(outputDir, ".dev.vars"), serialized ?? ""); -// -// const vars = wrangler.unstable_getVarsForDev( -// path.join(outputDir, "wrangler.json"), -// undefined, -// {}, -// undefined, -// true -// ); -// const binding = vars.KEY; -// const got = -// binding && "value" in binding && typeof binding.value === "string" -// ? binding.value -// : undefined; -// expect(got).toBe(value); -// }); -// }); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/local-env.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/local-env.spec.ts new file mode 100644 index 00000000000..94e72b05308 --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/__tests__/local-env.spec.ts @@ -0,0 +1,41 @@ +import { describe, test } from "vitest"; +import { resolveLocalBindings } from "../local-env"; + +const workerConfig = { + env: { + SECRET: { type: "secret" as const }, + MISSING: { type: "secret" as const }, + TEXT: { type: "text" as const, value: "configured" }, + HYPERDRIVE: { + type: "hyperdrive" as const, + id: "hyperdrive-id", + }, + }, +}; + +describe("local bindings", () => { + test("replaces only declared secrets and reports missing ones", ({ + expect, + }) => { + const result = resolveLocalBindings(workerConfig.env, { + SECRET: "local", + TEXT: "not-used", + UNDECLARED: "ignored", + CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: + "postgres://localhost/database", + }); + + expect(result).toEqual({ + bindings: { + SECRET: { type: "text", value: "local" }, + TEXT: { type: "text", value: "configured" }, + HYPERDRIVE: { + type: "hyperdrive", + id: "hyperdrive-id", + dev: { connectionString: "postgres://localhost/database" }, + }, + }, + missingSecrets: ["MISSING"], + }); + }); +}); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts index c56d6ed4714..795723ca90a 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/resolve-plugin-config.spec.ts @@ -12,13 +12,13 @@ import type { WorkersResolvedConfig, } from "../plugin-config"; -const { readBuildOutputWorkersMock } = vi.hoisted(() => ({ - readBuildOutputWorkersMock: vi.fn(), +const { readBuildOutputPreviewMock } = vi.hoisted(() => ({ + readBuildOutputPreviewMock: vi.fn(), })); vi.mock("../build-output-preview", async (importOriginal) => ({ ...(await importOriginal()), - readBuildOutputWorkers: readBuildOutputWorkersMock, + readBuildOutputPreview: readBuildOutputPreviewMock, })); const FIXTURES_ROOT = path.resolve(__dirname, "fixtures", "plugin-config"); @@ -28,7 +28,7 @@ describe("resolvePluginConfig", () => { let root: string; beforeEach(() => { - readBuildOutputWorkersMock.mockReset(); + readBuildOutputPreviewMock.mockReset(); fs.mkdirSync(FIXTURES_ROOT, { recursive: true }); root = fs.realpathSync(fs.mkdtempSync(path.join(FIXTURES_ROOT, "case-"))); }); @@ -207,8 +207,7 @@ describe("resolvePluginConfig", () => { expect(result.type).toBe("workers"); if (result.type === "workers") { expect( - result.environmentNameToWorkerMap.get(result.entryWorkerEnvironmentName) - ?.config.entrypoint + result.environmentNameToWorkerMap.get("ssr")?.config.entrypoint ).toBe(path.join(root, "src/index.ts")); expect( result.environmentNameToWorkerMap.get("viteOnlyWorker")?.config @@ -399,9 +398,7 @@ describe("resolvePluginConfig", () => { )) as WorkersResolvedConfig; expect( - result.environmentNameToWorkerMap.get( - result.prerenderWorkerEnvironmentName ?? "" - )?.config.name + result.environmentNameToWorkerMap.get("prerender")?.config.name ).toBe("prerender-2024-12-30"); }); @@ -481,6 +478,52 @@ describe("resolvePluginConfig", () => { ).toBe("auxiliary-worker"); } }); + + test("resolves Cloudflare environment files from Vite's envDir without mutating process.env", async ({ + expect, + }) => { + writeEntryConfig(); + const envDir = path.join(root, "environment"); + fs.mkdirSync(envDir); + fs.writeFileSync( + path.join(envDir, ".env.production"), + "CLOUDFLARE_VITE_FORCE_LOCAL=true" + ); + fs.writeFileSync( + path.join(envDir, ".dev.vars.production"), + "SECRET=from-dev-vars" + ); + vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", undefined); + + const result = await resolvePluginConfig( + { remoteBindings: true }, + { root, envDir: "environment" }, + buildEnv + ); + + expect(result.remoteBindings).toBe(false); + expect(result.localEnv.values.CLOUDFLARE_VITE_FORCE_LOCAL).toBe("true"); + expect(result.devVars?.SECRET).toBe("from-dev-vars"); + expect(process.env.CLOUDFLARE_VITE_FORCE_LOCAL).toBeUndefined(); + }); + + test("honours Vite's envDir false option", async ({ expect }) => { + writeEntryConfig(); + fs.writeFileSync( + path.join(root, ".env.production"), + "CLOUDFLARE_VITE_FORCE_LOCAL=true" + ); + vi.stubEnv("CLOUDFLARE_VITE_FORCE_LOCAL", undefined); + + const result = await resolvePluginConfig( + { remoteBindings: true }, + { root, envDir: false }, + buildEnv + ); + + expect(result.remoteBindings).toBe(true); + }); + test("rejects duplicate Vite environment names", async ({ expect }) => { writeEntryConfig({ namedWorkers: [ @@ -505,19 +548,20 @@ describe("resolvePluginConfig", () => { test("preview reads only the Build Output Specification", async ({ expect, }) => { - readBuildOutputWorkersMock.mockResolvedValue([ - { - source: "build-output", - config: { - type: "worker", - name: "preview-worker", - compatibilityDate: "2024-12-30", + readBuildOutputPreviewMock.mockResolvedValue({ + settings: undefined, + workers: [ + { + config: { + type: "worker", + name: "preview-worker", + compatibilityDate: "2024-12-30", + }, + assetsDir: undefined, + bundle: undefined, }, - settings: undefined, - assetsDir: undefined, - bundle: undefined, - }, - ]); + ], + }); const result = await resolvePluginConfig( {} satisfies PluginConfig, @@ -526,7 +570,7 @@ describe("resolvePluginConfig", () => { ); expect(result.type).toBe("preview"); - expect(readBuildOutputWorkersMock).toHaveBeenCalledWith(root, false); + expect(readBuildOutputPreviewMock).toHaveBeenCalledWith(root, false); if (result.type === "preview") { expect(result.workers[0]?.config.name).toBe("preview-worker"); } @@ -536,7 +580,10 @@ describe("resolvePluginConfig", () => { expect, }) => { vi.stubEnv("CLOUDFLARE_VITE_BUILD", "true"); - readBuildOutputWorkersMock.mockResolvedValue([]); + readBuildOutputPreviewMock.mockResolvedValue({ + settings: undefined, + workers: [], + }); await resolvePluginConfig( {} satisfies PluginConfig, @@ -544,25 +591,71 @@ describe("resolvePluginConfig", () => { { mode: "production", command: "serve", isPreview: true } ); - expect(readBuildOutputWorkersMock).toHaveBeenCalledWith(root, true); + expect(readBuildOutputPreviewMock).toHaveBeenCalledWith(root, true); }); - test("preview ignores Vite Worker config and reads only Build Output", async ({ + test("preview loads local env using the mode recorded in Build Output", async ({ expect, }) => { - readBuildOutputWorkersMock.mockResolvedValue([ - { - source: "build-output", - config: { - type: "worker", - name: "entry-worker", - compatibilityDate: "2024-12-30", + fs.writeFileSync( + path.join(root, ".env.production"), + "PREVIEW_MODE_VALUE=preview-mode" + ); + fs.writeFileSync( + path.join(root, ".env.staging"), + "PREVIEW_MODE_VALUE=build-mode" + ); + fs.writeFileSync( + path.join(root, ".dev.vars.production"), + "PREVIEW_SECRET=preview-mode" + ); + fs.writeFileSync( + path.join(root, ".dev.vars.staging"), + "PREVIEW_SECRET=build-mode" + ); + readBuildOutputPreviewMock.mockResolvedValue({ + settings: { type: "settings", mode: "staging" }, + workers: [ + { + config: { + type: "worker", + name: "preview-worker", + compatibilityDate: "2024-12-30", + }, + assetsDir: undefined, + bundle: undefined, }, - settings: undefined, - assetsDir: undefined, - bundle: undefined, - }, - ]); + ], + }); + + const result = await resolvePluginConfig( + {} satisfies PluginConfig, + { root }, + { mode: "production", command: "serve", isPreview: true } + ); + + expect(result.type).toBe("preview"); + expect(result.localEnv.values.PREVIEW_MODE_VALUE).toBe("build-mode"); + expect(result.devVars?.PREVIEW_SECRET).toBe("build-mode"); + }); + + test("preview uses all Workers read from Build Output", async ({ + expect, + }) => { + readBuildOutputPreviewMock.mockResolvedValue({ + settings: undefined, + workers: [ + { + config: { + type: "worker", + name: "entry-worker", + compatibilityDate: "2024-12-30", + }, + assetsDir: undefined, + bundle: undefined, + }, + ], + }); const result = await resolvePluginConfig( { auxiliaryWorkers: { ignoredDuringPreview: {} }, diff --git a/packages/vite-plugin-cloudflare/src/build-output-preview.ts b/packages/vite-plugin-cloudflare/src/build-output-preview.ts index 8e2f59bd4dc..f2f7c8967a4 100644 --- a/packages/vite-plugin-cloudflare/src/build-output-preview.ts +++ b/packages/vite-plugin-cloudflare/src/build-output-preview.ts @@ -17,25 +17,29 @@ export interface Bundle { } export interface BuildOutputPreviewWorker { - source: "build-output"; config: ParsedOutputWorkerConfig; - settings: ParsedOutputSettingsConfig | undefined; assetsDir: string | undefined; bundle: Bundle | undefined; } +export interface BuildOutputPreview { + settings: ParsedOutputSettingsConfig | undefined; + workers: BuildOutputPreviewWorker[]; +} + /** * Read the Build Output Specification and select the Workers used for preview. * Every auxiliary Worker is always included. During prerendering, the * `prerender` Worker replaces the `default` entry Worker when present. */ -export async function readBuildOutputWorkers( +export async function readBuildOutputPreview( root: string, isPrerender: boolean -): Promise { +): Promise { // `settings` comes from the top-level `config.json` holding project-level // settings (`account_id`, `compliance_region`) shared by every Worker. It - // also carries the `mode` the build ran in. + // also carries the `mode` the build ran in, which preview uses to select + // local env files. const { workers, settings } = await readBuildOutput(root); const defaultWorker = workers[DEFAULT_WORKER_DIRECTORY_NAME]; const entryWorker = isPrerender @@ -49,16 +53,12 @@ export async function readBuildOutputWorkers( ); }) .map(([_, worker]) => worker); + const selectedWorkers = [entryWorker, ...auxiliaryWorkers]; - return [entryWorker, ...auxiliaryWorkers].map((worker) => - toPreviewWorker(worker, settings) - ); + return { settings, workers: selectedWorkers.map(toPreviewWorker) }; } -function toPreviewWorker( - worker: BuildOutputWorker, - settings: ParsedOutputSettingsConfig | undefined -): BuildOutputPreviewWorker { +function toPreviewWorker(worker: BuildOutputWorker): BuildOutputPreviewWorker { const { manifest } = worker.config; let bundle: Bundle | undefined; if (manifest && worker.bundleDir) { @@ -70,9 +70,7 @@ function toPreviewWorker( } return { - source: "build-output", config: worker.config, - settings, assetsDir: worker.assetsDir, bundle, }; diff --git a/packages/vite-plugin-cloudflare/src/context.ts b/packages/vite-plugin-cloudflare/src/context.ts index 04ff39c8eee..4665e8f5cbc 100644 --- a/packages/vite-plugin-cloudflare/src/context.ts +++ b/packages/vite-plugin-cloudflare/src/context.ts @@ -289,7 +289,7 @@ export class PluginContext { get settings(): ParsedInputSettingsConfig | undefined { if (this.resolvedPluginConfig.type === "preview") { - return this.resolvedPluginConfig.workers[0]?.settings; + return this.resolvedPluginConfig.settings; } return this.resolvedPluginConfig.parsedConfig.settings; diff --git a/packages/vite-plugin-cloudflare/src/dev-vars.ts b/packages/vite-plugin-cloudflare/src/dev-vars.ts deleted file mode 100644 index 223ccbc2a15..00000000000 --- a/packages/vite-plugin-cloudflare/src/dev-vars.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as path from "node:path"; -import * as wrangler from "wrangler"; -import type { - AssetsOnlyResolvedConfig, - WorkersResolvedConfig, -} from "./plugin-config"; -import type { Unstable_Config } from "wrangler"; - -/** - * Gets any variables with which to augment the Worker config in preview mode. - * - * Calls `unstable_getVarsForDev` with the current Cloudflare environment to get local dev variables from the .dev.vars/.env/process.env. - * When `secrets` is defined in the Worker config, only declared secrets are loaded. - */ -export function getLocalDevVarsForPreview( - config: Unstable_Config, - cloudflareEnv: string | undefined -): string | undefined { - const dotDevDotVars = wrangler.unstable_getVarsForDev( - config.configPath, - undefined, // We don't currently support setting a list of custom `.env` files. - {}, // Don't pass actual vars since these will be loaded from the wrangler.json. - cloudflareEnv, - false, - config.secrets - ); - const dotDevDotVarsEntries = Array.from(Object.entries(dotDevDotVars)); - if (dotDevDotVarsEntries.length > 0) { - const dotDevDotVarsContent = dotDevDotVarsEntries - .map(([key, { value }]) => { - // `value` is typed as optional but `unstable_getVarsForDev` - // always populates it; `?? ""` narrows the type for `quoteForDotenv`. - return `${key}=${quoteForDotenv(value?.toString() ?? "")}\n`; - }) - .join(""); - return dotDevDotVarsContent; - } -} - -/** - * Quote a value so that wrangler reads it back unchanged when it parses - * `dist//.dev.vars` at preview time. - * - * Strategy: To quote the string pick the first quote character that does not - * appear in the value (single → backtick → double). Throw if a value cannot - * be losslessly serialized rather than silently corrupting it. - * - * @internal exported for tests - */ -export function quoteForDotenv(value: string): string { - if (!value.includes("'")) { - return `'${value}'`; - } - if (!value.includes("`")) { - return `\`${value}\``; - } - if (!value.includes('"') && !/[\\\n\r]/.test(value)) { - return `"${value}"`; - } - throw new Error( - "Unable to serialize value to .dev.vars: contains every supported quote character or unsafe escape sequence." - ); -} - -/** - * Returns `true` if the `changedFile` matches a `.dev.vars` or `.env` file. - */ -export function hasLocalDevVarsFileChanged( - { - configPaths, - cloudflareEnv, - }: AssetsOnlyResolvedConfig | WorkersResolvedConfig, - changedFilePath: string -) { - return [...configPaths].some((configPath) => { - const configDir = path.dirname(configPath); - return [ - ".dev.vars", - ".env", - ...(cloudflareEnv - ? [`.dev.vars.${cloudflareEnv}`, `.env.${cloudflareEnv}`] - : []), - ].some( - (localDevFile) => changedFilePath === path.join(configDir, localDevFile) - ); - }); -} diff --git a/packages/vite-plugin-cloudflare/src/local-env.ts b/packages/vite-plugin-cloudflare/src/local-env.ts new file mode 100644 index 00000000000..95f8bc6c48e --- /dev/null +++ b/packages/vite-plugin-cloudflare/src/local-env.ts @@ -0,0 +1,54 @@ +import type { ParsedInputWorkerConfig } from "@cloudflare/config"; + +type WorkerBindings = NonNullable; + +export interface ResolvedLocalBindings { + bindings: WorkerBindings; + missingSecrets: string[]; +} + +/** + * Resolves local binding values from the loaded local env. This replaces + * declared secret placeholders with text bindings and applies Hyperdrive + * connection-string overrides. Missing secrets are omitted so Miniflare + * doesn't receive an unresolved deployment-only placeholder. + * + * @param configuredBindings The Worker's configured bindings. + * @param localEnv Values resolved from `.env` files and the process environment. + * @param devVars Values from the selected `.dev.vars` file, which are used exclusively when provided. + * @returns The complete bindings and any secrets missing a local value. + */ +export function resolveLocalBindings( + configuredBindings: ParsedInputWorkerConfig["env"], + localEnv: Record, + devVars?: Record +): ResolvedLocalBindings { + const bindings: WorkerBindings = {}; + const missingSecrets: string[] = []; + const localValues = devVars ?? localEnv; + + for (const [name, binding] of Object.entries(configuredBindings ?? {})) { + if (binding.type === "hyperdrive") { + const connectionString = + localEnv[`CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_${name}`]; + bindings[name] = connectionString + ? { ...binding, dev: { connectionString } } + : binding; + continue; + } + + if (binding.type !== "secret") { + bindings[name] = binding; + continue; + } + + const localValue = localValues[name]; + if (localValue !== undefined) { + bindings[name] = { type: "text", value: localValue }; + } else { + missingSecrets.push(name); + } + } + + return { bindings, missingSecrets }; +} diff --git a/packages/vite-plugin-cloudflare/src/miniflare-options.ts b/packages/vite-plugin-cloudflare/src/miniflare-options.ts index 2e109212490..178fe05a741 100644 --- a/packages/vite-plugin-cloudflare/src/miniflare-options.ts +++ b/packages/vite-plugin-cloudflare/src/miniflare-options.ts @@ -28,6 +28,7 @@ import { VITE_PROXY_WORKER_NAME, } from "./constants"; import { getInputInspectorPort } from "./debug"; +import { resolveLocalBindings } from "./local-env"; import { additionalModuleRE } from "./plugins/additional-modules"; import { getRemoteBindings } from "./remote-bindings"; import { ENVIRONMENT_NAME_HEADER } from "./shared"; @@ -44,12 +45,17 @@ import type { PreviewPluginContext, WorkersPluginContext, } from "./context"; +import type { ResolvedLocalBindings } from "./local-env"; import type { PersistState } from "./plugin-config"; -import type { ParsedInputWorkerConfig } from "@cloudflare/config"; +import type { + ParsedInputWorkerConfig, + ParsedOutputWorkerConfig, +} from "@cloudflare/config"; import type { RemoteBindingsLogger, RemoteProxySessionData, } from "@cloudflare/remote-bindings"; +import type { LoadedEnv } from "@cloudflare/workers-utils/local-env"; import type { MiniflareOptions, WorkerdStructuredLog, @@ -222,6 +228,16 @@ export async function getDevMiniflareOptions( entryWorkerConfig, resolvedViteConfig ); + const resolvedLocalBindingsByWorkerName = resolveLocalBindingsByWorkerName( + ctx.allWorkerConfigs, + resolvedPluginConfig.localEnv, + resolvedPluginConfig.devVars + ); + warnForMissingSecrets( + resolvedLocalBindingsByWorkerName, + resolvedViteConfig.mode, + viteDevServer.config.logger + ); const [routerWorkerManifest, assetWorkerManifest, viteProxyWorkerManifest] = await Promise.all([ getInternalWorkerManifest(ROUTER_WORKER_PATH), @@ -498,8 +514,13 @@ export async function getDevMiniflareOptions( assets: _assets, ...config } = worker.config; + const resolvedLocalBindings = + resolvedLocalBindingsByWorkerName.get(worker.config.name); + assert(resolvedLocalBindings); const env: MiniflareEnv = {}; - for (const [name, binding] of Object.entries(config.env ?? {})) { + for (const [name, binding] of Object.entries( + resolvedLocalBindings.bindings + )) { if (binding.type === "hyperdrive") { assert( binding.dev?.connectionString !== undefined, @@ -757,6 +778,16 @@ export async function getPreviewMiniflareOptions( vitePreviewServer ); const { resolvedPluginConfig, resolvedViteConfig } = ctx; + const resolvedLocalBindingsByWorkerName = resolveLocalBindingsByWorkerName( + ctx.allWorkerConfigs, + resolvedPluginConfig.localEnv, + resolvedPluginConfig.devVars + ); + warnForMissingSecrets( + resolvedLocalBindingsByWorkerName, + resolvedViteConfig.mode, + vitePreviewServer.config.logger + ); // TODO: Add Container Miniflare configuration when Containers are supported by // cloudflare.config.ts. const workers: WorkerOptions[] = await Promise.all( @@ -773,9 +804,9 @@ export async function getPreviewMiniflareOptions( name: workerConfig.name, bindings: bindings ?? {}, complianceRegion: toRemoteComplianceRegion( - previewWorker.settings?.complianceRegion + resolvedPluginConfig.settings?.complianceRegion ), - account_id: previewWorker.settings?.accountId, + account_id: resolvedPluginConfig.settings?.accountId, profileDir: resolvedViteConfig.root, }, preExistingRemoteProxySessionData ?? null, @@ -795,8 +826,14 @@ export async function getPreviewMiniflareOptions( } const { manifest: _manifest, assets, exports, ...config } = workerConfig; + const resolvedLocalBindings = resolvedLocalBindingsByWorkerName.get( + workerConfig.name + ); + assert(resolvedLocalBindings); const env: MiniflareEnv = {}; - for (const [name, binding] of Object.entries(config.env ?? {})) { + for (const [name, binding] of Object.entries( + resolvedLocalBindings.bindings + )) { if (binding.type === "hyperdrive") { assert( binding.dev?.connectionString !== undefined, @@ -893,6 +930,45 @@ export async function getPreviewMiniflareOptions( return miniflareOptions; } +function resolveLocalBindingsByWorkerName( + workerConfigs: Array, + localEnv: LoadedEnv, + devVars: Record | undefined +): Map { + const resolvedLocalBindingsByWorkerName = new Map< + string, + ResolvedLocalBindings + >(); + for (const config of workerConfigs) { + resolvedLocalBindingsByWorkerName.set( + config.name, + resolveLocalBindings(config.env, localEnv.values, devVars) + ); + } + return resolvedLocalBindingsByWorkerName; +} + +function warnForMissingSecrets( + resolvedLocalBindingsByWorkerName: Map, + mode: string, + logger: vite.Logger +): void { + for (const [ + workerName, + { missingSecrets }, + ] of resolvedLocalBindingsByWorkerName) { + if (missingSecrets.length === 0) { + continue; + } + logger.warn( + `Missing required secrets for Worker "${workerName}": ${missingSecrets.join(", ")}. ` + + `Add them to one of \`.dev.vars.${mode}\`, \`.dev.vars\`, ` + + `\`.env.${mode}.local\`, \`.env.${mode}\`, \`.env.local\`, or \`.env\`. ` + + "Alternatively, set them as environment variables." + ); + } +} + /** * A Miniflare logger that forwards messages onto a Vite logger. */ diff --git a/packages/vite-plugin-cloudflare/src/plugin-config.ts b/packages/vite-plugin-cloudflare/src/plugin-config.ts index 23583d8152e..4036ed792cb 100644 --- a/packages/vite-plugin-cloudflare/src/plugin-config.ts +++ b/packages/vite-plugin-cloudflare/src/plugin-config.ts @@ -15,16 +15,19 @@ import { DEFAULT_COMPAT_DATE, getWorkerNameFromProject, } from "@cloudflare/workers-utils"; +import { loadDevVars, loadEnv } from "@cloudflare/workers-utils/local-env"; import { defu } from "defu"; -import * as vite from "vite"; -import { readBuildOutputWorkers } from "./build-output-preview"; +import { readBuildOutputPreview } from "./build-output-preview"; import { hasNodeJsCompat, NodeJsCompat } from "./nodejs-compat"; import type { BuildOutputPreviewWorker } from "./build-output-preview"; import type { ParsedConfigExports, ParsedInputWorkerConfig, + ParsedOutputSettingsConfig, } from "@cloudflare/config"; import type { StaticRouting } from "@cloudflare/workers-shared/utils/types"; +import type { LoadedEnv } from "@cloudflare/workers-utils/local-env"; +import type * as vite from "vite"; export type PersistState = boolean | { path: string }; export type TunnelConfig = { @@ -163,6 +166,8 @@ interface BaseResolvedConfig { experimental: Pick; remoteBindings: boolean; tunnel: TunnelConfig; + localEnv: LoadedEnv; + devVars: Record | undefined; } interface NonPreviewResolvedConfig extends BaseResolvedConfig { @@ -188,6 +193,7 @@ export interface WorkersResolvedConfig extends NonPreviewResolvedConfig { export interface PreviewResolvedConfig extends BaseResolvedConfig { type: "preview"; + settings: ParsedOutputSettingsConfig | undefined; workers: BuildOutputPreviewWorker[]; } @@ -352,6 +358,16 @@ export async function resolvePluginConfig( "mode" in userConfig && typeof userConfig.mode === "string" ? userConfig.mode : viteEnv.mode; + const root = userConfig.root ? path.resolve(userConfig.root) : process.cwd(); + const envDir = resolveEnvDir(root, userConfig.envDir); + const preview = viteEnv.isPreview + ? await readBuildOutputPreview(root, !!process.env.CLOUDFLARE_VITE_BUILD) + : undefined; + const localEnvMode = preview ? preview.settings?.mode : mode; + const [localEnv, devVars] = await Promise.all([ + loadEnv(envDir, localEnvMode), + loadDevVars(envDir, localEnvMode), + ]); const types = normalizeTypes(pluginConfig.types); const shared = { persistState: pluginConfig.persistState ?? true, @@ -368,35 +384,25 @@ export async function resolvePluginConfig( headersAndRedirectsDevModeSupport: pluginConfig.experimental?.headersAndRedirectsDevModeSupport, }, + localEnv, + devVars, }; - const root = userConfig.root ? path.resolve(userConfig.root) : process.cwd(); - const prefixedEnv = vite.loadEnv(mode, root, [ - "CLOUDFLARE_", - // TODO: Remove deprecated WRANGLER prefix support in next major version - "WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_", - ]); - - // Make Cloudflare-prefixed environment variables available while resolving - // config and starting development services. - Object.assign(process.env, prefixedEnv); // The `cf-vite` delegate binary's `--local` flag sets this env var to // force remote bindings off, overriding any `remoteBindings` value in the // plugin config (mirrors `wrangler dev --local`). const remoteBindings = - prefixedEnv.CLOUDFLARE_VITE_FORCE_LOCAL === "true" + localEnv.values.CLOUDFLARE_VITE_FORCE_LOCAL === "true" ? false : (pluginConfig.remoteBindings ?? true); - if (viteEnv.isPreview) { + if (preview !== undefined) { return { ...shared, remoteBindings, type: "preview", - workers: await readBuildOutputWorkers( - root, - !!process.env.CLOUDFLARE_VITE_BUILD - ), + settings: preview.settings, + workers: preview.workers, }; } @@ -639,6 +645,20 @@ function addAuxiliaryWorkers(options: { } } +/** + * Resolves Vite's environment directory before `configResolved` is available. + * + * @param root The already-resolved Vite root. + * @param envDir The user-provided Vite `envDir` option. + * @returns An absolute environment directory, or `false` when disabled. + */ +export function resolveEnvDir( + root: string, + envDir: string | false | undefined +): string | false { + return envDir === false ? false : path.resolve(root, envDir ?? "."); +} + const RESERVED_WORKER_EXPORT_NAMES = new Set(["default", "prerender"]); const RESERVED_WORKER_DIRECTORY_NAMES = new Set([ ...RESERVED_WORKER_EXPORT_NAMES, diff --git a/packages/vite-plugin-cloudflare/src/plugins/config.ts b/packages/vite-plugin-cloudflare/src/plugins/config.ts index 74f2153d3d3..b23c1be5499 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/config.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/config.ts @@ -4,6 +4,7 @@ import { getWorkerAssetsDir, getWorkerBundleDir, } from "@cloudflare/build-output-utils"; +import { getDevVarsCandidatePaths } from "@cloudflare/workers-utils/local-env"; import { normalizePath } from "vite"; import { hasAssetsConfigChanged } from "../asset-config"; import { createBuildApp } from "../build"; @@ -98,6 +99,13 @@ export const configPlugin = createPlugin("config", (ctx) => { // config changes, including the one that fixes the config, would be // ignored for the rest of the session. let restartInFlight = false; + const localDevVarsFiles = new Set( + getDevVarsCandidatePaths( + ctx.resolvedViteConfig.envDir, + ctx.resolvedViteConfig.mode + ) + ); + viteDevServer.watcher.add([...localDevVarsFiles]); const configChangedHandler = async (changedFilePath: string) => { assertIsNotPreview(ctx); @@ -106,9 +114,8 @@ export const configPlugin = createPlugin("config", (ctx) => { return; } - // TODO: Reinstate .env and .dev.vars watching when local variable - // loading is supported with cloudflare.config.ts. if ( + localDevVarsFiles.has(changedFilePath) || ctx.resolvedPluginConfig.configPaths.has(changedFilePath) || hasAssetsConfigChanged( ctx.resolvedPluginConfig, @@ -128,6 +135,8 @@ export const configPlugin = createPlugin("config", (ctx) => { }; viteDevServer.watcher.on("change", configChangedHandler); + viteDevServer.watcher.on("add", configChangedHandler); + viteDevServer.watcher.on("unlink", configChangedHandler); }, }; }); diff --git a/packages/vite-plugin-cloudflare/tsconfig.plugin.json b/packages/vite-plugin-cloudflare/tsconfig.plugin.json index 5f14e46e602..4b94e0be842 100644 --- a/packages/vite-plugin-cloudflare/tsconfig.plugin.json +++ b/packages/vite-plugin-cloudflare/tsconfig.plugin.json @@ -10,9 +10,6 @@ "src/__tests__/fixtures", // TODO: Remove this exclusion when Containers are supported by // cloudflare.config.ts. - "src/containers.ts", - // TODO: Remove this exclusion when `.env` and `.dev.vars` files are - // supported with cloudflare.config.ts. - "src/dev-vars.ts" + "src/containers.ts" ] }