diff --git a/docs/reference/config.md b/docs/reference/config.md index cbedbd546..fba8228e2 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -305,7 +305,7 @@ Custom names are used in the deploy manifest and in wrangler binding sync for lo #### `inference.build` -Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or custom build scripts. If omitted, the CLI uses the framework's default build command. +Override the build command. Useful for frameworks with their own CLIs (Nuxt, Astro) or static apps with custom build scripts. If omitted, the CLI uses the detected default build command for the current app type. ```json { "inference": { "build": "nuxt build --preset cloudflare-module" } } diff --git a/packages/void/src/cli/deploy.ts b/packages/void/src/cli/deploy.ts index 4d7156651..83a1ec0f9 100644 --- a/packages/void/src/cli/deploy.ts +++ b/packages/void/src/cli/deploy.ts @@ -204,6 +204,14 @@ export async function runDeploy(root: string, options?: RunDeployOptions): Promi const gitCommit = options?.skipBuild ? null : getGitCommit(root); if (preset) { + preset = { + ...preset, + buildCommand: resolveStaticBuildCommand( + preset, + voidConfig.inference?.build, + Boolean(options?.dir), + ), + }; await runStaticDeploy( root, config, @@ -220,6 +228,15 @@ export async function runDeploy(root: string, options?: RunDeployOptions): Promi } } +export function resolveStaticBuildCommand( + preset: StaticPreset, + buildOverride?: string, + hasExplicitDir = false, +): string | null { + if (hasExplicitDir) return null; + return buildOverride ?? preset.buildCommand; +} + async function streamDeploy( client: PlatformClient, projectId: string, diff --git a/packages/void/src/cli/preset.ts b/packages/void/src/cli/preset.ts index c22355573..faa831c7e 100644 --- a/packages/void/src/cli/preset.ts +++ b/packages/void/src/cli/preset.ts @@ -189,8 +189,9 @@ export function detectPreset( // 4. Vite/Vite+ in deps → static SPA if (pkg && hasViteLike(pkg)) { + const buildCommand = resolveProjectScriptCommand(root, "build"); return { - buildCommand: resolveProjectCommand(root, "vite build"), + buildCommand: buildCommand ?? resolveProjectCommand(root, "vite build"), outputDir: resolve(root, "dist"), appType: "spa", }; @@ -247,6 +248,31 @@ function getAllDeps(pkg: Record): Record { }; } +function detectPackageManager(root: string): "pnpm" | "yarn" | "bun" | "npm" { + if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm"; + if (existsSync(join(root, "yarn.lock"))) return "yarn"; + if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) return "bun"; + return "npm"; +} + +function resolveProjectScriptCommand(root: string, scriptName: string): string | null { + const pkg = readPackageJson(root); + const script = pkg?.scripts; + if (!script || typeof script !== "object" || Array.isArray(script)) return null; + if (typeof (script as Record)[scriptName] !== "string") return null; + + switch (detectPackageManager(root)) { + case "pnpm": + return `pnpm ${scriptName}`; + case "yarn": + return `yarn ${scriptName}`; + case "bun": + return `bun run ${scriptName}`; + default: + return `npm run ${scriptName}`; + } +} + function hasViteLike(pkg: Record): boolean { const deps = getAllDeps(pkg); return "vite" in deps || "vite-plus" in deps; diff --git a/packages/void/test/unit/deploy-project.test.ts b/packages/void/test/unit/deploy-project.test.ts index 2b8ea54f6..1ab0ee3c1 100644 --- a/packages/void/test/unit/deploy-project.test.ts +++ b/packages/void/test/unit/deploy-project.test.ts @@ -2,7 +2,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; -import { resolveDistDir, resolveWorkerDirName } from "../../src/cli/deploy"; +import { + resolveDistDir, + resolveStaticBuildCommand, + resolveWorkerDirName, +} from "../../src/cli/deploy"; import { FRAMEWORK_PRESETS } from "../../src/cli/preset"; import { getRequestedProjectSlug, @@ -144,6 +148,35 @@ describe("resolveDistDir", () => { }); }); +describe("resolveStaticBuildCommand", () => { + it("uses inference.build when provided", () => { + expect( + resolveStaticBuildCommand( + { + buildCommand: "vite build", + outputDir: "/tmp/dist", + appType: "static", + }, + "pnpm build", + ), + ).toBe("pnpm build"); + }); + + it("keeps explicit --dir deploys buildless even when inference.build is set", () => { + expect( + resolveStaticBuildCommand( + { + buildCommand: null, + outputDir: "/tmp/dist", + appType: "static", + }, + "pnpm build", + true, + ), + ).toBeNull(); + }); +}); + describe("resolveWorkerDirName", () => { afterEach(() => { while (tempDirs.length > 0) { diff --git a/packages/void/test/unit/preset.test.ts b/packages/void/test/unit/preset.test.ts index a3caa355f..0928665d6 100644 --- a/packages/void/test/unit/preset.test.ts +++ b/packages/void/test/unit/preset.test.ts @@ -148,6 +148,22 @@ describe("detectPreset", () => { }); }); + it("prefers the project build script for plain Vite SPAs", () => { + const root = setupProject({ + pkg: { + dependencies: { vite: "^6.0.0" }, + scripts: { build: "vite build && node scripts/postbuild.js" }, + }, + files: { "pnpm-lock.yaml": "lockfileVersion: '9.0'\n" }, + }); + const result = detectPreset(root); + expect(result).toEqual({ + buildCommand: "pnpm build", + outputDir: resolve(root, "dist"), + appType: "spa", + }); + }); + it("detects Vite+ SPA (no backend files)", () => { const root = setupProject({ pkg: { devDependencies: { "vite-plus": "^1.0.0" } },