diff --git a/.changeset/polish-vite-plugin-installation.md b/.changeset/polish-vite-plugin-installation.md new file mode 100644 index 0000000000..23960a8eb9 --- /dev/null +++ b/.changeset/polish-vite-plugin-installation.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Polish Cloudflare Vite plugin installation during autoconfig + +Projects using Vite 6.0.x were rejected by auto-configuration because the minimum supported version was set to 6.1.0 (the `@cloudflare/vite-plugin` peer dependency). The minimum version check is now 6.0.0, and when a project has Vite in the [6.0.0, 6.1.0) range, auto-configuration will automatically upgrade it to the latest 6.x before installing `@cloudflare/vite-plugin`. diff --git a/packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts b/packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts new file mode 100644 index 0000000000..5da1355bd8 --- /dev/null +++ b/packages/wrangler/src/__tests__/autoconfig/frameworks/utils/vite-plugin.test.ts @@ -0,0 +1,288 @@ +import * as cliPackages from "@cloudflare/cli/packages"; +import { beforeEach, describe, it, vi } from "vitest"; +import { getInstalledPackageVersion } from "../../../../autoconfig/frameworks/utils/packages"; +import { installCloudflareVitePlugin } from "../../../../autoconfig/frameworks/utils/vite-plugin"; +import type { MockInstance } from "vitest"; + +vi.mock("../../../../autoconfig/frameworks/utils/packages", () => ({ + getInstalledPackageVersion: vi.fn(), +})); + +describe("installCloudflareVitePlugin", () => { + let installSpy: MockInstance; + + beforeEach(() => { + installSpy = vi + .spyOn(cliPackages, "installPackages") + .mockImplementation(async () => {}); + }); + + describe("when Vite is not installed/detected", () => { + beforeEach(() => { + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + }); + + it("installs only @cloudflare/vite-plugin", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("does not attempt to upgrade Vite", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + }); + + describe("when Vite version is in [6.0.0, 6.1.0) range", () => { + it("upgrades Vite before installing the plugin for version 6.0.0", async ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(2); + expect(installSpy).toHaveBeenNthCalledWith( + 1, + "npm", + ["vite@^6.1.0"], + expect.objectContaining({ dev: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("upgrades Vite before installing the plugin for version 6.0.5", async ({ + expect, + }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.5"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(2); + expect(installSpy).toHaveBeenNthCalledWith( + 1, + "npm", + ["vite@^6.1.0"], + expect.objectContaining({ dev: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + }); + + describe("when Vite version is >= 6.1.0", () => { + it("does not upgrade Vite for version 6.1.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.1.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + + it("does not upgrade Vite for version 6.2.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.2.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + + it("does not upgrade Vite for version 7.0.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("7.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + }); + + describe("when Vite version is < 6.0.0", () => { + it("does not upgrade Vite for version 5.4.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("5.4.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).not.toHaveBeenCalledWith( + expect.anything(), + ["vite@^6.1.0"], + expect.anything() + ); + }); + + it("does not upgrade Vite for version 4.0.0", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("4.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledTimes(1); + expect(installSpy).toHaveBeenCalledWith( + "npm", + ["@cloudflare/vite-plugin"], + expect.objectContaining({ dev: true }) + ); + }); + }); + + describe("parameter forwarding", () => { + beforeEach(() => { + vi.mocked(getInstalledPackageVersion).mockReturnValue(undefined); + }); + + it("forwards isWorkspaceRoot to installPackages", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: true, + }); + + expect(installSpy).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ isWorkspaceRoot: true }) + ); + }); + + it("forwards isWorkspaceRoot when upgrading Vite", async ({ expect }) => { + vi.mocked(getInstalledPackageVersion).mockReturnValue("6.0.0"); + + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/test/project", + isWorkspaceRoot: true, + }); + + // Both the Vite upgrade and plugin install should have isWorkspaceRoot: true + expect(installSpy).toHaveBeenNthCalledWith( + 1, + expect.anything(), + ["vite@^6.1.0"], + expect.objectContaining({ isWorkspaceRoot: true }) + ); + expect(installSpy).toHaveBeenNthCalledWith( + 2, + expect.anything(), + ["@cloudflare/vite-plugin"], + expect.objectContaining({ isWorkspaceRoot: true }) + ); + }); + + it("forwards packageManager to installPackages", async ({ expect }) => { + await installCloudflareVitePlugin({ + packageManager: "pnpm", + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledWith( + "pnpm", + expect.anything(), + expect.anything() + ); + }); + + it("forwards different package managers correctly", async ({ expect }) => { + for (const pm of ["npm", "yarn", "pnpm", "bun"] as const) { + installSpy.mockClear(); + + await installCloudflareVitePlugin({ + packageManager: pm, + projectPath: "/test/project", + isWorkspaceRoot: false, + }); + + expect(installSpy).toHaveBeenCalledWith( + pm, + expect.anything(), + expect.anything() + ); + } + }); + + it("passes projectPath to getInstalledPackageVersion", async ({ + expect, + }) => { + await installCloudflareVitePlugin({ + packageManager: "npm", + projectPath: "/custom/path", + isWorkspaceRoot: false, + }); + + expect(getInstalledPackageVersion).toHaveBeenCalledWith( + "vite", + "/custom/path" + ); + }); + }); +}); diff --git a/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts b/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts index a46b5eb816..0ebfeb4d87 100644 --- a/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts +++ b/packages/wrangler/src/autoconfig/frameworks/all-frameworks.ts @@ -174,6 +174,9 @@ export const allKnownFrameworks = [ name: "vite", // Vite 6 introduced the Environment API which @cloudflare/vite-plugin requires // See: https://vite.dev/blog/announcing-vite6#experimental-environment-api + // (6.1.0 is the minimum version supported by the vite plugin: + // https://github.com/cloudflare/workers-sdk/blob/b9b7e9d9fe/packages/vite-plugin-cloudflare/package.json#L80 + // we anyways allow for `6.0.x` versions since we bump them to `^6.1.0` in the autoconfig process) minimumVersion: "6.0.0", maximumKnownMajorVersion: "8", }, diff --git a/packages/wrangler/src/autoconfig/frameworks/react-router.ts b/packages/wrangler/src/autoconfig/frameworks/react-router.ts index 4f891154f5..a781baa68e 100644 --- a/packages/wrangler/src/autoconfig/frameworks/react-router.ts +++ b/packages/wrangler/src/autoconfig/frameworks/react-router.ts @@ -10,6 +10,7 @@ import dedent from "ts-dedent"; import { logger } from "../../logger"; import { Framework } from "./framework-class"; import { transformViteConfig } from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; import type { ConfigurationOptions, ConfigurationResults, @@ -148,12 +149,9 @@ export class ReactRouter extends Framework { }: ConfigurationOptions): Promise { const viteEnvironmentKey = configPropertyName(this.frameworkVersion); if (!dryRun) { - await installPackages(packageManager.type, ["@cloudflare/vite-plugin"], { - dev: true, - startText: "Installing the Cloudflare Vite plugin", - doneText: `${brandColor(`installed`)} ${dim( - "@cloudflare/vite-plugin" - )}`, + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + projectPath, isWorkspaceRoot, }); diff --git a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts index dd93b99865..5b8f2baa43 100644 --- a/packages/wrangler/src/autoconfig/frameworks/tanstack.ts +++ b/packages/wrangler/src/autoconfig/frameworks/tanstack.ts @@ -1,7 +1,6 @@ -import { brandColor, dim } from "@cloudflare/cli/colors"; -import { installPackages } from "@cloudflare/cli/packages"; import { Framework } from "./framework-class"; import { transformViteConfig } from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; import type { ConfigurationOptions, ConfigurationResults, @@ -15,13 +14,10 @@ export class TanstackStart extends Framework { isWorkspaceRoot, }: ConfigurationOptions): Promise { if (!dryRun) { - await installPackages(packageManager.type, ["@cloudflare/vite-plugin"], { - dev: true, - startText: "Installing the Cloudflare Vite plugin", - doneText: `${brandColor(`installed`)} ${dim( - "@cloudflare/vite-plugin" - )}`, + await installCloudflareVitePlugin({ + packageManager: packageManager.type, isWorkspaceRoot, + projectPath, }); transformViteConfig(projectPath, { viteEnvironmentName: "ssr" }); diff --git a/packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts b/packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts new file mode 100644 index 0000000000..efe6364e45 --- /dev/null +++ b/packages/wrangler/src/autoconfig/frameworks/utils/vite-plugin.ts @@ -0,0 +1,51 @@ +import { brandColor, dim } from "@cloudflare/cli/colors"; +import { installPackages } from "@cloudflare/cli/packages"; +import semiver from "semiver"; +import { getInstalledPackageVersion } from "./packages"; +import type { PackageManager } from "../../../package-manager"; + +/** + * Installs the `@cloudflare/vite-plugin` package as a dev dependency + * + * If the project has Vite >= 6.0.0 but < 6.1.0 installed, it will first + * be updated to `^6.1.0` to ensure compatibility with the plugin. + * + * @param packageManager the type of package manager to use for installation + * @param projectPath the path of the project (used to check the installed Vite version) + * @param isWorkspaceRoot whether the current project is a workspace root + */ +export async function installCloudflareVitePlugin({ + packageManager, + projectPath, + isWorkspaceRoot, +}: { + packageManager: PackageManager["type"]; + projectPath: string; + isWorkspaceRoot: boolean; +}): Promise { + const viteVersion = getInstalledPackageVersion("vite", projectPath); + + if ( + viteVersion && + semiver(viteVersion, "6.0.0") >= 0 && + semiver(viteVersion, "6.1.0") < 0 + ) { + // If the vite version is between 6.0.0 and 6.1.0 lets bump it to + // the latest version of 6.x, in this way it will be compatible + // with the vite plugin (likely without causing any inconvenience) + await installPackages(packageManager, ["vite@^6.1.0"], { + dev: true, + startText: + "Updating the version of vite to be compatible with the Cloudflare Vite Plugin", + doneText: `${brandColor(`updated`)} ${dim("Vite")}`, + isWorkspaceRoot, + }); + } + + await installPackages(packageManager, ["@cloudflare/vite-plugin"], { + dev: true, + startText: "Installing the Cloudflare Vite plugin", + doneText: `${brandColor(`installed`)} ${dim("@cloudflare/vite-plugin")}`, + isWorkspaceRoot, + }); +} diff --git a/packages/wrangler/src/autoconfig/frameworks/vike.ts b/packages/wrangler/src/autoconfig/frameworks/vike.ts index 29d0ee34e1..ce8c508435 100644 --- a/packages/wrangler/src/autoconfig/frameworks/vike.ts +++ b/packages/wrangler/src/autoconfig/frameworks/vike.ts @@ -7,6 +7,7 @@ import { transformFile } from "@cloudflare/codemod"; import * as recast from "recast"; import { Framework } from "./framework-class"; import { isPackageInstalled } from "./utils/packages"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; import type { ConfigurationOptions, ConfigurationResults, @@ -48,9 +49,11 @@ export class Vike extends Framework { isWorkspaceRoot, } ); - await installPackages(packageManager.type, ["@cloudflare/vite-plugin"], { - dev: true, + + await installCloudflareVitePlugin({ + packageManager: packageManager.type, isWorkspaceRoot, + projectPath, }); addVikePhotonToConfigFile(projectPath); diff --git a/packages/wrangler/src/autoconfig/frameworks/vite.ts b/packages/wrangler/src/autoconfig/frameworks/vite.ts index 7613a33162..631b7323dc 100644 --- a/packages/wrangler/src/autoconfig/frameworks/vite.ts +++ b/packages/wrangler/src/autoconfig/frameworks/vite.ts @@ -1,10 +1,9 @@ -import { brandColor, dim } from "@cloudflare/cli/colors"; -import { installPackages } from "@cloudflare/cli/packages"; import { Framework } from "./framework-class"; import { checkIfViteConfigUsesCloudflarePlugin, transformViteConfig, } from "./utils/vite-config"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; import type { ConfigurationOptions, ConfigurationResults, @@ -22,13 +21,10 @@ export class Vite extends Framework { isWorkspaceRoot, }: ConfigurationOptions): Promise { if (!dryRun) { - await installPackages(packageManager.type, ["@cloudflare/vite-plugin"], { - dev: true, - startText: "Installing the Cloudflare Vite plugin", - doneText: `${brandColor(`installed`)} ${dim( - "@cloudflare/vite-plugin" - )}`, + await installCloudflareVitePlugin({ + packageManager: packageManager.type, isWorkspaceRoot, + projectPath, }); transformViteConfig(projectPath); diff --git a/packages/wrangler/src/autoconfig/frameworks/waku.ts b/packages/wrangler/src/autoconfig/frameworks/waku.ts index a99f1cb711..5eedd5ae0e 100644 --- a/packages/wrangler/src/autoconfig/frameworks/waku.ts +++ b/packages/wrangler/src/autoconfig/frameworks/waku.ts @@ -9,6 +9,7 @@ import { transformFile } from "@cloudflare/codemod"; import * as recast from "recast"; import dedent from "ts-dedent"; import { Framework } from "./framework-class"; +import { installCloudflareVitePlugin } from "./utils/vite-plugin"; import type { ConfigurationOptions, ConfigurationResults, @@ -26,16 +27,18 @@ export class Waku extends Framework { isWorkspaceRoot, }: ConfigurationOptions): Promise { if (!dryRun) { - await installPackages( - packageManager.type, - ["hono", "@cloudflare/vite-plugin"], - { - dev: true, - startText: "Installing additional dependencies", - doneText: `${brandColor("installed")}`, - isWorkspaceRoot, - } - ); + await installPackages(packageManager.type, ["hono"], { + dev: true, + startText: "Installing hono dependency", + doneText: `${brandColor("installed")}`, + isWorkspaceRoot, + }); + + await installCloudflareVitePlugin({ + packageManager: packageManager.type, + projectPath, + isWorkspaceRoot, + }); await createWakuServerFile(projectPath); await updateWakuConfig(projectPath);