Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/tidy-doors-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@cloudflare/vite-plugin": minor
---

Add a check to vite-plugin that ensures that the version of Wrangler being used internally is correct

In some pnpm setups it is possible for a different peer dependency version of Wrangler to leak and override the version that we require internally.
2 changes: 1 addition & 1 deletion packages/vite-plugin-cloudflare/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ There is a `seed()` helper to setup a clean copy of a fixture outside of the mon
The simplest test looks like:

```ts
const projectPath = await seed("basic", "pnpm");
const projectPath = await seed("basic", { pm: "pnpm" });

test("can serve a Worker request", async ({ expect, seed, runLongLived }) => {
const proc = await runLongLived("npm", "dev", projectPath);
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-cloudflare/e2e/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const commands = ["dev", "buildAndPreview"] as const;

describe("basic e2e tests", () => {
describe.each(packageManagers)('with "%s" package manager', async (pm) => {
const projectPath = seed("basic", pm);
const projectPath = seed("basic", { pm });

describe.each(commands)('with "%s" command', (command) => {
test.skipIf(isBuildAndPreviewOnWindows(command))(
Expand Down
2 changes: 1 addition & 1 deletion packages/vite-plugin-cloudflare/e2e/dynamic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const packageManagers = ["pnpm", "npm", "yarn"] as const;

describe("prebundling Node.js compatibility", () => {
describe.each(packageManagers)('with "%s" package manager', (pm) => {
const projectPath = seed("dynamic", pm);
const projectPath = seed("dynamic", { pm });

test("will not cause a reload on a dynamic import of a Node.js module", async ({
expect,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@cloudflare/vite-plugin-e2e-invalid-wrangler-version",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"buildAndPreview": "vite build && vite preview",
"dev": "vite",
"lint": "eslint .",
"preview": "vite preview"
},
"devDependencies": {
"@cloudflare/vite-plugin": "*",
"@cloudflare/workers-types": "^4.20250204.0",
"@eslint/js": "^9.19.0",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0",
"wrangler": "4.20.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
async fetch() {
return new Response("OK");
},
} satisfies ExportedHandler;
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,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,

/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
"types": ["@cloudflare/workers-types/2023-07-01", "vite/client"]
},
"include": ["src"]
}
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({ inspectorPort: false, persistState: false })],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "cloudflare-vite-e2e-invalid-wrangler-version",
"main": "./src/index.ts",
"compatibility_date": "2024-12-30",
"compatibility_flags": ["nodejs_compat"],
}
24 changes: 18 additions & 6 deletions packages/vite-plugin-cloudflare/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,15 @@ const strictPeerDeps = {
/** Seed a test project from a fixture. */
export function seed(
fixture: string,
pm: "pnpm" | "yarn" | "npm",
replacements: Record<string, string> = {}
{
pm,
replacements = {},
useStrictPeerDeps = true,
}: {
pm: "pnpm" | "yarn" | "npm";
replacements?: Record<string, string>;
useStrictPeerDeps?: boolean;
}
) {
const root = inject("root");
const projectPath = path.resolve(root, fixture, pm);
Expand All @@ -57,9 +64,13 @@ export function seed(
debuglog("Fixing up replacements in seeded files");
await fixupReplacements(projectPath, replacements);
debuglog("Updated vite-plugin version in package.json");
runCommand(`${pm} install ${strictPeerDeps[pm]}`, projectPath, {
attempts: 2,
});
runCommand(
`${pm} install ${useStrictPeerDeps ? strictPeerDeps[pm] : ""}`,
projectPath,
{
attempts: 2,
}
);
debuglog("Installed node modules");
}, 200_000);

Expand Down Expand Up @@ -179,7 +190,8 @@ async function updateVitePluginAndWranglerVersion(projectPath: string) {
if (pkg[field]?.["@cloudflare/vite-plugin"]) {
pkg[field]["@cloudflare/vite-plugin"] = vitePluginPackage.version;
}
if (pkg[field]?.["wrangler"]) {
// Some fixtures require the current version of wrangler to be installed
if (pkg[field]?.["wrangler"] === "*") {
pkg[field]["wrangler"] = wranglerPackage.version;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { runLongLived, seed } from "./helpers";
// The test here just makes sure that the validation takes place.
// Unit tests for the validation are in `src/__tests__/validate-worker-environment-options.spec.ts`
describe("validate Worker environment options", () => {
const projectPath = seed("invalid-worker-environment-options", "pnpm");
const projectPath = seed("invalid-worker-environment-options", {
pm: "pnpm",
});

test("throws an error for invalid environment options", async ({
expect,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, test } from "vitest";
import { runLongLived, seed } from "./helpers.js";

describe("invalid Wrangler version e2e tests", () => {
// Only test with pnpm;
// npm and yarn don't hoist peer dependencies in the same way as pnpm;
// so having a different peer Wrangler doesn't mess up the internal dependency
const projectPath = seed("invalid-wrangler-version", {
pm: "pnpm",
useStrictPeerDeps: false,
});

test("`vite dev` will error when peer installed wrangler version overrides the expected internal dependency", async ({
expect,
}) => {
const proc = await runLongLived("pnpm", "dev", projectPath);
expect(await proc.exitCode).not.toBe(0);
expect(proc.stderr).toMatch(
/The installed version of Wrangler \(4\.20\.0\) does not satisfy the peer dependency required by @cloudflare\/vite-plugin \(\^\d+\.\d+\.\d+\)/
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { runLongLived, seed } from "./helpers";
// This test validates that warnings are displayed when nodejs_compat is missing
// and Node.js imports are used without the compatibility flag.
describe("nodejs_compat warnings", () => {
const projectPath = seed("nodejs-compat-warnings", "pnpm");
const projectPath = seed("nodejs-compat-warnings", { pm: "pnpm" });

test("displays warnings if Node.js built-ins are imported and the nodejs_compat flag is not enabled", async ({
expect,
Expand Down
12 changes: 8 additions & 4 deletions packages/vite-plugin-cloudflare/e2e/remote-bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ if (!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_TOKEN) {
"<<REMOTE_WORKER_PLACEHOLDER_ALT>>": `preserve-e2e-vite-remote-alt`,
};

const projectPath = seed("remote-bindings", "pnpm", replacements);
const projectPath = seed("remote-bindings", { pm: "pnpm", replacements });

beforeAll(async () => {
try {
Expand Down Expand Up @@ -139,7 +139,9 @@ if (!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_TOKEN) {
});

describe("remote bindings without actually establishing a remote connection", () => {
const projectPath = seed("remote-bindings-config-account-id", "pnpm");
const projectPath = seed("remote-bindings-config-account-id", {
pm: "pnpm",
});

test("for connection to remote bindings during dev the account_id present in the wrangler config file is used", async ({
expect,
Expand All @@ -161,7 +163,9 @@ if (!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_TOKEN) {
});

describe("failure to connect to remote bindings", () => {
const projectPath = seed("remote-bindings-incorrect-r2-config", "pnpm");
const projectPath = seed("remote-bindings-incorrect-r2-config", {
pm: "pnpm",
});

describe.each(commands)('with "%s" command', (command) => {
// On Windows the path for the miniflare dependency gets pretty long and this fails in node < 22.7
Expand All @@ -187,7 +191,7 @@ if (!process.env.CLOUDFLARE_ACCOUNT_ID || !process.env.CLOUDFLARE_API_TOKEN) {
}

describe("remote bindings disabled", () => {
const projectPath = seed("remote-bindings-disabled", "pnpm");
const projectPath = seed("remote-bindings-disabled", { pm: "pnpm" });

describe.each(commands)('with "%s" command', (command) => {
// On Windows the path for the miniflare dependency gets pretty long and this fails in node < 22.7
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test } from "vitest";
import { runLongLived, seed } from "./helpers";

describe("unresolved main entry file", () => {
const projectPath = seed("unresolved-main", "pnpm");
const projectPath = seed("unresolved-main", { pm: "pnpm" });

test("throws an error when the main entry file cannot be resolved", async ({
expect,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { runLongLived, seed } from "./helpers";
describe("during development wrangler config files are validated", () => {
const noWranglerConfigAuxProjectPath = seed(
"no-wrangler-config-for-auxiliary-worker",
"pnpm"
{ pm: "pnpm" }
);
test("for auxiliary workers", async ({ expect }) => {
const proc = await runLongLived(
Expand Down
2 changes: 2 additions & 0 deletions packages/vite-plugin-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,11 @@
"@cloudflare/workers-tsconfig": "workspace:*",
"@cloudflare/workers-types": "catalog:default",
"@types/node": "catalog:vite-plugin",
"@types/semver": "^7.5.1",
"@types/ws": "^8.5.13",
"magic-string": "^0.30.12",
"mlly": "^1.7.4",
"semver": "^7.7.1",
"tree-kill": "^1.2.2",
"tsdown": "0.16.3",
"typescript": "catalog:default",
Expand Down
36 changes: 36 additions & 0 deletions packages/vite-plugin-cloudflare/src/assert-wrangler-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { satisfies } from "semver";

/**
* Asserts that the installed version of Wrangler that gets pulled in at runtime by the `@cloudflare/vite-plugin`
* matches the version that `@cloudflare/vite-plugin` actually depends upon.
*
* This can sometime be broken by package managers that deduplicate dependencies, such as `pnpm`.
*/
export async function assertWranglerVersion() {
const installedVersion = (
await import("wrangler/package.json", {
with: { type: "json" },
})
).default.version;

const ourPackageJson = (
await import("../package.json", {
with: { type: "json" },
})
).default;

const peerDependency = ourPackageJson.peerDependencies.wrangler;

if (peerDependency.startsWith("workspace:")) {
// We are running in the monorepo, so these deps are not yet computed to specific semver strings.
// We don't need to worry in this case and can skip the check.
return;
}

if (!satisfies(installedVersion, peerDependency)) {
throw new Error(
`The installed version of Wrangler (${installedVersion}) does not satisfy the peer dependency required by @cloudflare/vite-plugin (${peerDependency}).\n` +
`Please install wrangler@${peerDependency}.`
);
}
}
4 changes: 2 additions & 2 deletions packages/vite-plugin-cloudflare/src/deploy-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import assert from "node:assert";
import * as fs from "node:fs";
import * as path from "node:path";
import * as vite from "vite";
import { unstable_readConfig } from "wrangler";
import * as wrangler from "wrangler";
import type {
AssetsOnlyResolvedConfig,
WorkersResolvedConfig,
Expand Down Expand Up @@ -31,7 +31,7 @@ export function getWorkerConfigs(root: string) {
path.dirname(deployConfigPath),
configPath
);
return unstable_readConfig({ config: resolvedConfigPath });
return wrangler.unstable_readConfig({ config: resolvedConfigPath });
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/vite-plugin-cloudflare/src/dev-vars.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as path from "node:path";
import { unstable_getVarsForDev } from "wrangler";
import * as wrangler from "wrangler";
import type {
AssetsOnlyResolvedConfig,
WorkersResolvedConfig,
Expand All @@ -14,7 +14,7 @@ export function getLocalDevVarsForPreview(
configPath: string | undefined,
cloudflareEnv: string | undefined
): string | undefined {
const dotDevDotVars = unstable_getVarsForDev(
const dotDevDotVars = wrangler.unstable_getVarsForDev(
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.
Expand Down
10 changes: 6 additions & 4 deletions packages/vite-plugin-cloudflare/src/export-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert";
import { unstable_getDurableObjectClassNameToUseSQLiteMap } from "wrangler";
import * as wrangler from "wrangler";
import { debuglog } from "./utils";
import type { CloudflareDevEnvironment } from "./cloudflare-environment";
import type { Worker, WorkersResolvedConfig } from "./plugin-config";
Expand Down Expand Up @@ -40,9 +40,11 @@ function getWorkerNameToDurableObjectExportsMap(
workers.map((worker) => [
worker.config.name,
new Set(
unstable_getDurableObjectClassNameToUseSQLiteMap(
worker.config.migrations
).keys()
wrangler
.unstable_getDurableObjectClassNameToUseSQLiteMap(
worker.config.migrations
)
.keys()
),
])
);
Expand Down
3 changes: 3 additions & 0 deletions packages/vite-plugin-cloudflare/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as vite from "vite";
import { assertWranglerVersion } from "./assert-wrangler-version";
import { PluginContext } from "./context";
import { resolvePluginConfig } from "./plugin-config";
import { additionalModulesPlugin } from "./plugins/additional-modules";
Expand Down Expand Up @@ -30,6 +31,8 @@ const sharedContext: SharedContext = {
isRestartingDevServer: false,
};

await assertWranglerVersion();

/**
* Vite plugin that enables a full-featured integration between Vite and the Cloudflare Workers runtime.
*
Expand Down
Loading
Loading