Skip to content

Commit

Permalink
improve and test vite-plugin-cloudflare .dev.vars files support
Browse files Browse the repository at this point in the history
the changes here add tests for making sure that the vite plugin correctly
reads secrets from `.dev.vars` files with and without a specified environment

as part of this wragler's `unstable_getMiniflareWorkerOptions` utility needed
to also be updated to accept the environment name as its second parameter
  • Loading branch information
dario-piotrowicz committed Jan 23, 2025
1 parent d7210ec commit 8689cad
Show file tree
Hide file tree
Showing 24 changed files with 281 additions and 16 deletions.
9 changes: 9 additions & 0 deletions .changeset/orange-camels-hunt_vite-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/vite-plugin": patch
---

add full support for `.dev.vars` files

this change makes sure that `.dev.vars` files work for a specified environment, it also
copies the target `.dev.vars` file (which might be environment specific, eg: `.dev.vars.prod`)
to the worker's dist directory, always named `.dev.vars` (so that `vite preview` can pick it up)
9 changes: 9 additions & 0 deletions .changeset/orange-camels-hunt_wrangler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"wrangler": minor
---

allow `unstable_getMiniflareWorkerOptions` to always accept an `env` argument

the `unstable_getMiniflareWorkerOptions` utility, when accepting a config object as its first argument,
doesn't accept an `env` one, the changes here make sure it does since even if a config object is passed
the `env` one is still relevant for picking up variables from potential `.dev.vars` files
16 changes: 16 additions & 0 deletions packages/vite-plugin-cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,17 @@ It accepts an optional `PluginConfig` parameter.
By default, the environment name is the Worker name with `-` characters replaced with `_`.
Setting the name here will override this.

## Secrets

The vite plugin supports local development via secrets using the [`.dev.vars` file convention](https://developers.cloudflare.com/workers/configuration/secrets/#local-development-with-secrets).

Meaning that when you run `vite dev` or `vite preview` the plugin picks up secrets set in your `.dev.vars` files and makes them available to your application.

> [!NOTE]
> The `vite build` command copies the potential `.dev.vars` target file into the same output directory in which it creates the build output `wrangler.json`, this copied file is the one that `vite preview` will then use
To instead make such secrets available to deployed applications follow the [relative Cloudflare documentation](https://developers.cloudflare.com/workers/configuration/secrets/#secrets-on-deployed-workers).

## Cloudflare environments

A Worker config file may contain configuration for multiple [Cloudflare environments](https://developers.cloudflare.com/workers/wrangler/environments/).
Expand Down Expand Up @@ -414,6 +425,11 @@ This is because the environment name is automatically appended to the top-level
> The default Vite environment name for a Worker is always the top-level Worker name.
> This enables you to reference the Worker consistently in your Vite config when using multiple Cloudflare environments.
> [!NOTE]
> Setting `CLOUDFLARE_ENV` also effects the `.dev.vars` being read, as files ending with a dot and the environment
> name get prioritized over the standard `.dev.vars` ones (e.g. if I run `CLOUDFLARE_ENV=staging vite dev`, the vite plugin will
> look for a `.dev.vars.staging` file to gather the secrets from, falling back to `.dev.vars` in case the file doesn't exist)
Cloudflare environments can also be used in development.
For example, you could run `CLOUDFLARE_ENV=development vite dev`.
It is common to use the default top-level environment as the development environment and then add additional environments as necessary.
Expand Down
4 changes: 4 additions & 0 deletions packages/vite-plugin-cloudflare/playground/dev-vars/.dev.vars
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ENV_NAME = ""
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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ENV_NAME = "staging"
MY_DEV_VAR_A = "my .dev.vars staging variable A"
MY_DEV_VAR_B = "my .dev.vars staging variable B"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CLOUDFLARE_ENV=staging
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
!.dev.vars
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import fs from "node:fs";
import { describe, expect, test } from "vitest";
import { getJsonResponse, isBuild, testDir } from "../../__test-utils__";

test("reading variables from a standard .dev.vars file", 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",
},
});
});

describe.runIf(isBuild)("build output files", () => {
test("the .dev.vars file has been copied over", async () => {
const srcDevVarsPath = `${testDir}/.dev.vars`;
const distDevVarsPath = `${testDir}/dist/worker/.dev.vars`;

const distDevVarsExists = fs.existsSync(distDevVarsPath);
expect(distDevVarsExists).toBe(true);

const srcDevVarsContent = fs.readFileSync(srcDevVarsPath, "utf-8");
const distDevVarsContent = fs.readFileSync(distDevVarsPath, "utf-8");
expect(distDevVarsContent).toEqual(srcDevVarsContent);
});

test("secrets from .dev.vars haven't been inlined in the js output file", async () => {
const distIndexPath = `${testDir}/dist/worker/index.js`;

const distIndexContent = fs.readFileSync(distIndexPath, "utf-8");
expect(distIndexContent).not.toContain("my .dev.vars variable");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import fs from "node:fs";
import { describe, expect, test } from "vitest";
import { getJsonResponse, isBuild, testDir } from "../../../__test-utils__";

test("reading variables from a staging .dev.vars file", 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",
},
});
});

describe.runIf(isBuild)("build output files", () => {
test("the .dev.vars.staging file has been copied over as .dev.vars", async () => {
const srcDevVarsStagingPath = `${testDir}/.dev.vars.staging`;
const distDevVarsPath = `${testDir}/dist/worker/.dev.vars`;
const distDevVarsStagingPath = `${distDevVarsPath}.staging`;

const distDevVarsExists = fs.existsSync(distDevVarsPath);
expect(distDevVarsExists).toBe(true);

const srcDevVarsStagingContent = fs.readFileSync(
srcDevVarsStagingPath,
"utf-8"
);
const distDevVarsContent = fs.readFileSync(distDevVarsPath, "utf-8");
expect(distDevVarsContent).toEqual(srcDevVarsStagingContent);

const distDevVarsStagingExists = fs.existsSync(distDevVarsStagingPath);
expect(distDevVarsStagingExists).toBe(false);
});
});
20 changes: 20 additions & 0 deletions packages/vite-plugin-cloudflare/playground/dev-vars/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@playground/dev-vars",
"private": true,
"type": "module",
"scripts": {
"build": "vite build --app",
"check:types": "tsc --build",
"dev": "vite dev",
"prepreview": "node ./copy-dev-vars-to-dist.mjs",
"preview": "vite preview"
},
"devDependencies": {
"@cloudflare/vite-plugin": "workspace:*",
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "^4.20241230.0",
"typescript": "catalog:vite-plugin",
"vite": "catalog:vite-plugin",
"wrangler": "catalog:vite-plugin"
}
}
10 changes: 10 additions & 0 deletions packages/vite-plugin-cloudflare/playground/dev-vars/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default {
async fetch(_req, env) {
const { ENV_NAME, MY_DEV_VAR_A, MY_DEV_VAR_B, MY_DEV_VAR_C } = env;
const dotDevDotVarsVariables = { MY_DEV_VAR_A, MY_DEV_VAR_B, MY_DEV_VAR_C };
const extension = ENV_NAME ? `.${ENV_NAME}` : "";
return Response.json({
[`variables present in .dev.vars${extension}`]: dotDevDotVarsVariables,
});
},
} satisfies ExportedHandler<Env>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.worker.json" }
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["@cloudflare/workers-tsconfig/base.json"],
"include": ["vite.config.ts", "__tests__"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": ["@cloudflare/workers-tsconfig/worker.json"],
"include": ["src", "worker-configuration.d.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "http://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"outputs": ["dist/**"]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
plugins: [cloudflare({ persistState: false })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { cloudflare } from "@cloudflare/vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
mode: "with-specified-env",
plugins: [cloudflare({ persistState: false })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Generated by Wrangler by running `wrangler types`

interface Env {
ENV_NAME: string;
MY_DEV_VAR_A: string;
MY_DEV_VAR_B: string;
MY_DEV_VAR_C: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name = "worker"
main = "./src/index.ts"
compatibility_date = "2024-12-30"

[env.staging]
50 changes: 49 additions & 1 deletion packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,25 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin {
}

config = workerConfig;

if (workerConfig.configPath) {
copyDotDevDotVarsFileToOutputDir(
workerConfig.configPath,
resolvedPluginConfig.cloudflareEnv,
this.emitFile
);
}
} else if (this.environment.name === "client") {
const assetsOnlyConfig = resolvedPluginConfig.config;

assetsOnlyConfig.assets.directory = ".";

const filesToAssetsIgnore = ["wrangler.json", ".dev.vars"];

this.emitFile({
type: "asset",
fileName: ".assetsignore",
source: "wrangler.json",
source: `${filesToAssetsIgnore.join("\n")}\n`,
});

config = assetsOnlyConfig;
Expand Down Expand Up @@ -297,3 +307,41 @@ export function cloudflare(pluginConfig: PluginConfig = {}): vite.Plugin {
},
};
}

/**
* Copies a potential `.dev.vars` target over to the worker's output directory
* (so that it can get picked up by `vite preview`)
*
* Note: This resolves the .dev.vars file path following the same logic
* as `loadDotEnv` in `/packages/wrangler/src/config/index.ts`
* the two need to be kept in sync
*
* @param configPath the path to the worker's wrangler config file
* @param cloudflareEnv the target cloudflare environment
* @param emitFile vite's emitFile function (for saving the file)
*/
function copyDotDevDotVarsFileToOutputDir(
configPath: string,
cloudflareEnv: string | undefined,
emitFile: vite.Rollup.PluginContext["emitFile"]
) {
const configDir = path.dirname(configPath);

const defaultDotDevDotVarsPath = `${configDir}/.dev.vars`;
const inputDotDevDotVarsPath = `${defaultDotDevDotVarsPath}${!cloudflareEnv ? "" : `.${cloudflareEnv}`}`;

const targetPath = fs.existsSync(inputDotDevDotVarsPath)
? inputDotDevDotVarsPath
: fs.existsSync(defaultDotDevDotVarsPath)
? defaultDotDevDotVarsPath
: null;

if (targetPath) {
const dotDevDotVarsContent = fs.readFileSync(targetPath);
emitFile({
type: "asset",
fileName: ".dev.vars",
source: dotDevDotVarsContent,
});
}
}
11 changes: 7 additions & 4 deletions packages/vite-plugin-cloudflare/src/miniflare-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,13 @@ export function getDevMiniflareOptions(
resolvedPluginConfig.type === "workers"
? Object.entries(resolvedPluginConfig.workers).map(
([environmentName, workerConfig]) => {
const miniflareWorkerOptions = unstable_getMiniflareWorkerOptions({
...workerConfig,
assets: undefined,
});
const miniflareWorkerOptions = unstable_getMiniflareWorkerOptions(
{
...workerConfig,
assets: undefined,
},
resolvedPluginConfig.cloudflareEnv
);

const { ratelimits, ...workerOptions } =
miniflareWorkerOptions.workerOptions;
Expand Down
22 changes: 12 additions & 10 deletions packages/vite-plugin-cloudflare/src/plugin-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface WorkerConfig extends BaseConfig {
interface BasePluginConfig {
configPaths: Set<string>;
persistState: PersistState;
cloudflareEnv?: string;
}

interface AssetsOnlyPluginConfig extends BasePluginConfig {
Expand Down Expand Up @@ -82,7 +83,11 @@ export function resolvePluginConfig(
const configPaths = new Set<string>();
const persistState = pluginConfig.persistState ?? true;
const root = userConfig.root ? path.resolve(userConfig.root) : process.cwd();
const { CLOUDFLARE_ENV } = vite.loadEnv(viteEnv.mode, root, "");
const { CLOUDFLARE_ENV: cloudflareEnv } = vite.loadEnv(
viteEnv.mode,
root,
""
);

const configPath = pluginConfig.configPath
? path.resolve(root, pluginConfig.configPath)
Expand All @@ -93,14 +98,10 @@ export function resolvePluginConfig(
`Config not found. Have you created a wrangler.json(c) or wrangler.toml file?`
);

const entryWorkerResolvedConfig = getWorkerConfig(
configPath,
CLOUDFLARE_ENV,
{
visitedConfigPaths: configPaths,
isEntryWorker: true,
}
);
const entryWorkerResolvedConfig = getWorkerConfig(configPath, cloudflareEnv, {
visitedConfigPaths: configPaths,
isEntryWorker: true,
});

if (entryWorkerResolvedConfig.type === "assets-only") {
return {
Expand Down Expand Up @@ -129,7 +130,7 @@ export function resolvePluginConfig(
for (const auxiliaryWorker of pluginConfig.auxiliaryWorkers ?? []) {
const workerResolvedConfig = getWorkerConfig(
path.resolve(root, auxiliaryWorker.configPath),
CLOUDFLARE_ENV,
cloudflareEnv,
{
visitedConfigPaths: configPaths,
}
Expand Down Expand Up @@ -167,5 +168,6 @@ export function resolvePluginConfig(
entryWorker: entryWorkerResolvedConfig,
auxiliaryWorkers: auxiliaryWorkersResolvedConfigs,
},
cloudflareEnv,
};
}
3 changes: 2 additions & 1 deletion packages/wrangler/src/api/integrations/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ export function unstable_getMiniflareWorkerOptions(
env?: string
): Unstable_MiniflareWorkerOptions;
export function unstable_getMiniflareWorkerOptions(
config: Config
config: Config,
env?: string
): Unstable_MiniflareWorkerOptions;
export function unstable_getMiniflareWorkerOptions(
configOrConfigPath: string | Config,
Expand Down
Loading

0 comments on commit 8689cad

Please sign in to comment.