diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts new file mode 100644 index 0000000..26e85d4 --- /dev/null +++ b/packages/cli/src/dev.ts @@ -0,0 +1,87 @@ +import { createServer } from "vite"; +import { join } from "node:path"; +import { createLogger } from "./logger.ts"; +import { vueTuiDevPlugin } from "./vite-plugin.ts"; +import { extractBundle, type MemoryFiles } from "./bundle-extractor.ts"; +import { createProcessManager } from "./process-manager.ts"; + +async function waitForBundle(env: { memoryFiles: MemoryFiles }): Promise { + while (env.memoryFiles.files.size === 0) { + await new Promise((r) => setTimeout(r, 100)); + } +} + +export async function dev(entry?: string) { + const logger = createLogger(); + logger.info("Starting vue-tui dev server..."); + + const server = await createServer({ + plugins: [vueTuiDevPlugin({ entry })], + }); + + await server.listen(); + const port = server.config.server.port!; + logger.info(`Vite dev server on port ${port}`); + + const clientEnv = server.environments?.client as unknown as { + memoryFiles: MemoryFiles; + }; + + if (!clientEnv?.memoryFiles) { + logger.error("bundledDev environment not available. Ensure Vite 8+ with bundledDev support."); + process.exit(1); + } + + logger.info("Waiting for initial bundle..."); + await waitForBundle(clientEnv); + + const outDir = join(process.cwd(), "node_modules", ".vue-tui", "dev"); + let bundlePath = await extractBundle(clientEnv.memoryFiles, outDir); + logger.info("Bundle ready."); + + let crashed = false; + + const pm = createProcessManager({ + bundlePath, + hmrPort: port, + logger, + onExit(code) { + if (code !== 0 && code !== null) { + crashed = true; + logger.info(`Process exited with code ${code}. Waiting for file changes...`); + } + }, + }); + + pm.spawn(); + + // Listen for full reload requests from child + server.hot.on("vue-tui:request-reload", async () => { + const newPath = await extractBundle(clientEnv.memoryFiles, outDir); + pm.setBundlePath(newPath); + pm.restart(); + }); + + // Re-spawn after crash on next successful bundle update + setInterval(async () => { + if (!crashed || pm.running) return; + try { + const newPath = await extractBundle(clientEnv.memoryFiles, outDir); + pm.setBundlePath(newPath); + crashed = false; + pm.spawn(); + } catch { + // Build still broken, keep waiting + } + }, 500); + + // Graceful shutdown + const shutdown = async () => { + await pm.shutdown(); + await server.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 09dd62a..d33af26 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,25 @@ #!/usr/bin/env node -const entry = process.argv[2]; -console.log(`vue-tui dev ${entry ?? "(auto-detect)"} — not yet implemented`); +import { existsSync } from "node:fs"; +import { dev } from "./dev.ts"; + +const args = process.argv.slice(2); +const command = args[0]; + +if (command !== "dev" && command !== undefined) { + console.error(`Unknown command: ${command}. Usage: vue-tui dev [entry]`); + process.exit(1); +} + +const explicitEntry = args[1]; + +function resolveEntry(): string | undefined { + if (explicitEntry) return explicitEntry; + const conventions = ["src/main.ts", "src/main.tsx", "src/index.ts", "src/index.tsx"]; + return conventions.find((p) => existsSync(p)); +} + +dev(resolveEntry()).catch((err) => { + console.error(err); + process.exit(1); +});