diff --git a/.changeset/viteplus-project-commands.md b/.changeset/viteplus-project-commands.md new file mode 100644 index 0000000000..d6eebda3bd --- /dev/null +++ b/.changeset/viteplus-project-commands.md @@ -0,0 +1,7 @@ +--- +"@cloudflare/autoconfig": patch +--- + +Run the project-local Vite+ executable for Vite+ projects + +Use `vp dev` and `vp build` when Vite+ is declared. Its Vite alias does not expose a `vite` executable, so invoking `vite` can fail with pnpm or cause npm to fetch a separate Vite. Preserve framework-owned commands and custom scripts. diff --git a/packages/autoconfig/src/details/index.ts b/packages/autoconfig/src/details/index.ts index 2e9b494774..7efdae5473 100644 --- a/packages/autoconfig/src/details/index.ts +++ b/packages/autoconfig/src/details/index.ts @@ -209,10 +209,18 @@ export async function getDetailsForAutoConfig({ packageManager, devCommand: useStaticAssets ? undefined - : getProjectCommand(detectedFramework.devCommand, packageManager), + : getProjectCommand( + detectedFramework.devCommand, + packageManager, + packageJson + ), buildCommand: useStaticAssets ? undefined - : getProjectCommand(detectedFramework.buildCommand, packageManager), + : getProjectCommand( + detectedFramework.buildCommand, + packageManager, + packageJson + ), env: framework.env, workerName: getWorkerName(packageJson?.name, projectPath), }; @@ -285,17 +293,32 @@ export async function getDetailsForAutoConfig({ * * @param command The detected project command * @param packageManager The package manager to use for command prefixes + * @param packageJson The optional project manifest used to detect Vite+ * @returns A runnable project command if detected, undefined otherwise */ -function getProjectCommand( +export function getProjectCommand( command: string | undefined, - packageManager: PackageManager + packageManager: PackageManager, + packageJson?: PackageJSON ): string | undefined { if (!command) { return undefined; } const { type, dlx, npx } = packageManager; + if ( + packageJson?.dependencies?.["vite-plus"] || + packageJson?.devDependencies?.["vite-plus"] + ) { + if ( + ["vite", "vite dev", `${npx} vite`, `${npx} vite dev`].includes(command) + ) { + return `${npx} vp dev`; + } + if (command === "vite build" || command === `${npx} vite build`) { + return `${npx} vp build`; + } + } for (const packageManagerCommandPrefix of [type, dlx.join(" "), npx]) { if (command.startsWith(packageManagerCommandPrefix)) { diff --git a/packages/autoconfig/src/run.ts b/packages/autoconfig/src/run.ts index 1c51e84eda..6e2d164691 100644 --- a/packages/autoconfig/src/run.ts +++ b/packages/autoconfig/src/run.ts @@ -22,6 +22,7 @@ import { assertNonConfigured, confirmAutoConfigDetails, displayAutoConfigDetails, + getProjectCommand, } from "./details"; import { isFrameworkSupported, @@ -165,9 +166,17 @@ export async function runAutoConfig( } const { npx } = packageManager; - const buildCommand = + let buildCommand = dryRunConfigurationResults.buildCommandOverride ?? autoConfigDetails.buildCommand; + // Preserve shell commands while normalizing Vite selected by framework overrides. + if (buildCommand === "vite build" || buildCommand === `${npx} vite build`) { + buildCommand = getProjectCommand( + buildCommand, + packageManager, + autoConfigDetails.packageJson + ); + } const autoConfigSummary = await buildOperationsSummary( { ...autoConfigDetails, outputDir: autoConfigDetails.outputDir }, diff --git a/packages/autoconfig/tests/details/vite-plus-commands.test.ts b/packages/autoconfig/tests/details/vite-plus-commands.test.ts new file mode 100644 index 0000000000..df6c7aa4be --- /dev/null +++ b/packages/autoconfig/tests/details/vite-plus-commands.test.ts @@ -0,0 +1,35 @@ +import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { describe, test } from "vitest"; +import { getProjectCommand } from "../../src/details"; + +describe("Vite+ commands", () => { + test.for(["npx", "pnpm", "yarn", "bunx"])( + "uses the local toolchain through %s", + (npx, { expect }) => { + const manager = { ...NpmPackageManager, npx }; + const pkg = { devDependencies: { "vite-plus": "1.4.0" } }; + expect(getProjectCommand("vite", manager, pkg)).toBe(`${npx} vp dev`); + expect(getProjectCommand("vite dev", manager, pkg)).toBe(`${npx} vp dev`); + expect(getProjectCommand("vite build", manager, pkg)).toBe( + `${npx} vp build` + ); + expect(getProjectCommand(`${npx} vite build`, manager, pkg)).toBe( + `${npx} vp build` + ); + } + ); + test("preserves framework scripts and projects without Vite+", ({ + expect, + }) => { + const pkg = { dependencies: { "vite-plus": "1.4.0" } }; + expect(getProjectCommand("astro build", NpmPackageManager, pkg)).toBe( + "npx astro build" + ); + expect(getProjectCommand("npm run build", NpmPackageManager, pkg)).toBe( + "npm run build" + ); + expect(getProjectCommand("vite build", NpmPackageManager, {})).toBe( + "npx vite build" + ); + }); +}); diff --git a/packages/autoconfig/tests/run.test.ts b/packages/autoconfig/tests/run.test.ts index 0a5b3ea2e1..6724f6b3a8 100644 --- a/packages/autoconfig/tests/run.test.ts +++ b/packages/autoconfig/tests/run.test.ts @@ -1,6 +1,9 @@ import { existsSync, readFileSync } from "node:fs"; import * as cliPackages from "@cloudflare/cli-shared-helpers/packages"; -import { NpmPackageManager } from "@cloudflare/workers-utils"; +import { + NpmPackageManager, + PnpmPackageManager, +} from "@cloudflare/workers-utils"; import { mockConsoleMethods, runInTempDir, @@ -59,6 +62,82 @@ describe("runAutoConfig()", () => { runInTempDir(); const std = mockConsoleMethods(); + it.for([ + { vitePlus: false, override: false }, + { vitePlus: true, override: false }, + { vitePlus: false, override: true }, + { vitePlus: true, override: true }, + ])( + "preserves custom build commands with Vite+=$vitePlus and override=$override", + async ({ vitePlus, override }, { expect }) => { + const buildCommand = "echo 'built' > build.txt"; + const packageJson = { + name: "static-app", + devDependencies: vitePlus ? { "vite-plus": "1.4.0" } : {}, + }; + await seed({ "package.json": JSON.stringify(packageJson) }); + const framework = getFrameworkClassInstance("static"); + vi.spyOn(framework, "configure").mockReturnValue({ + buildTool: "wrangler", + workerConfig: {}, + buildConfig: { assetsDirectory: "public" }, + ...(override && { buildCommandOverride: buildCommand }), + }); + const summary = await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "static-app", + framework, + outputDir: "public", + buildCommand: override ? "npm run build" : buildCommand, + packageManager: NpmPackageManager, + packageJson, + }, + { context: createMockContext(), dryRun: true } + ); + expect(summary.buildCommand).toBe(buildCommand); + } + ); + + it.for([ + { packageManager: NpmPackageManager, vitePlus: true }, + { packageManager: PnpmPackageManager, vitePlus: true }, + { packageManager: NpmPackageManager, vitePlus: false }, + ])( + "uses the vinext build override with $packageManager.type and Vite+=$vitePlus", + async ({ packageManager, vitePlus }, { expect }) => { + const packageJson = { + name: "next-app", + dependencies: { next: "16.1.0" }, + devDependencies: vitePlus ? { "vite-plus": "1.4.0" } : {}, + }; + await seed({ + "package.json": JSON.stringify(packageJson), + "node_modules/next/package.json": JSON.stringify({ + name: "next", + version: "16.1.0", + }), + }); + const summary = await runAutoConfig( + { + configured: false, + projectPath: process.cwd(), + workerName: "next-app", + framework: getFrameworkClassInstance("next"), + outputDir: ".next", + buildCommand: `${packageManager.npx} next build`, + packageManager, + packageJson, + }, + { context: createMockContext(), dryRun: true } + ); + expect(summary.buildCommand).toBe( + `${packageManager.npx} ${vitePlus ? "vp" : "vite"} build` + ); + } + ); + it("rejects autoconfiguration for an unsupported framework", async ({ expect, }) => {