diff --git a/packages/vinext/src/cli.ts b/packages/vinext/src/cli.ts index db562d1c1..e8f6e9d66 100644 --- a/packages/vinext/src/cli.ts +++ b/packages/vinext/src/cli.ts @@ -20,7 +20,7 @@ import path, { toSlash } from "pathslash"; import fs from "node:fs"; import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; -import { execFileSync } from "node:child_process"; +import { execFileSync, spawn } from "node:child_process"; import { randomBytes } from "node:crypto"; import { detectPackageManager, @@ -32,6 +32,7 @@ import { runCheck, formatReport } from "./check.js"; import { init as runInit, getReactUpgradeDeps } from "./init.js"; import { resolveInitOptions } from "./init-platform.js"; import { loadDotenv } from "./config/dotenv.js"; +import { findEmittedWranglerConfig } from "./utils/emitted-wrangler-config.js"; import { createRscCompatibilityId, findVinextNextConfigInPlugins, @@ -747,6 +748,14 @@ async function start() { const port = parsed.port ?? parseInt(process.env.PORT ?? "3000", 10); const host = parsed.hostname ?? "0.0.0.0"; + const cwd = process.cwd(); + const wranglerConfig = findEmittedWranglerConfig(cwd); + + if (wranglerConfig) { + console.log(`\n vinext start (wrangler ${wranglerConfig}, port ${port})\n`); + await startCloudflareWorker({ config: wranglerConfig, port, host, cwd }); + return; + } console.log(`\n vinext start (port ${port})\n`); @@ -757,7 +766,34 @@ async function start() { await startProdServer({ port, host, - outDir: path.resolve(process.cwd(), "dist"), + outDir: path.resolve(cwd, "dist"), + }); +} + +function startCloudflareWorker(opts: { + config: string; + port: number; + host: string; + cwd: string; +}): Promise { + const wranglerJs = path.join(opts.cwd, "node_modules", "wrangler", "bin", "wrangler.js"); + const args = ["dev", "--config", opts.config, "--port", String(opts.port), "--ip", opts.host]; + const child = fs.existsSync(wranglerJs) + ? spawn(process.execPath, [wranglerJs, ...args], { + stdio: "inherit", + cwd: opts.cwd, + }) + : spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", ...args], { + stdio: "inherit", + cwd: opts.cwd, + shell: process.platform === "win32", + }); + return new Promise((resolve, reject) => { + child.on("error", reject); + child.on("exit", (code) => { + if (code === 0 || code === null) resolve(); + else reject(new Error(`wrangler exited ${code}`)); + }); }); } diff --git a/packages/vinext/src/utils/emitted-wrangler-config.ts b/packages/vinext/src/utils/emitted-wrangler-config.ts new file mode 100644 index 000000000..029a3e1d5 --- /dev/null +++ b/packages/vinext/src/utils/emitted-wrangler-config.ts @@ -0,0 +1,28 @@ +import fs from "node:fs"; +import path from "pathslash"; + +/** + * Cloudflare Vite builds emit `dist//wrangler.json` + * (or `dist/server/wrangler.json`). The Node `vinext start` server + * looks for `dist/server/index.js` / `entry.js` and misses that output. + */ +export function findEmittedWranglerConfig(cwd: string): string | null { + const dist = path.join(cwd, "dist"); + if (!fs.existsSync(dist)) return null; + + const serverCfg = path.join(dist, "server", "wrangler.json"); + if (fs.existsSync(serverCfg)) return serverCfg; + + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dist, { withFileTypes: true }); + } catch { + return null; + } + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const candidate = path.join(dist, entry.name, "wrangler.json"); + if (fs.existsSync(candidate)) return candidate; + } + return null; +} diff --git a/tests/emitted-wrangler-config.test.ts b/tests/emitted-wrangler-config.test.ts new file mode 100644 index 000000000..1664685ef --- /dev/null +++ b/tests/emitted-wrangler-config.test.ts @@ -0,0 +1,31 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vite-plus/test"; +import { findEmittedWranglerConfig } from "../packages/vinext/src/utils/emitted-wrangler-config.js"; + +describe("findEmittedWranglerConfig", () => { + it("finds dist//wrangler.json", () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-wrangler-")); + fs.mkdirSync(path.join(cwd, "dist", "my-app"), { recursive: true }); + fs.writeFileSync(path.join(cwd, "dist", "my-app", "wrangler.json"), "{}"); + expect(findEmittedWranglerConfig(cwd)).toBe(path.join(cwd, "dist", "my-app", "wrangler.json")); + fs.rmSync(cwd, { recursive: true, force: true }); + }); + + it("prefers dist/server/wrangler.json", () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-wrangler-")); + fs.mkdirSync(path.join(cwd, "dist", "server"), { recursive: true }); + fs.mkdirSync(path.join(cwd, "dist", "other"), { recursive: true }); + fs.writeFileSync(path.join(cwd, "dist", "server", "wrangler.json"), "{}"); + fs.writeFileSync(path.join(cwd, "dist", "other", "wrangler.json"), "{}"); + expect(findEmittedWranglerConfig(cwd)).toBe(path.join(cwd, "dist", "server", "wrangler.json")); + fs.rmSync(cwd, { recursive: true, force: true }); + }); + + it("returns null without a Cloudflare build", () => { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "vinext-wrangler-")); + expect(findEmittedWranglerConfig(cwd)).toBeNull(); + fs.rmSync(cwd, { recursive: true, force: true }); + }); +});