feat(cli): add orchestrator and CLI entry point

Create dev.ts orchestrator wiring Vite dev server, bundle extractor, and
process manager together with HMR reload and crash-recovery logic.
Replace index.ts placeholder with full CLI entry supporting `vue-tui dev [entry]`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-24 19:57:47 +08:00
parent 2b7d1250f4
commit d319e77540
2 changed files with 110 additions and 2 deletions
+87
View File
@@ -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<void> {
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);
}
+23 -2
View File
@@ -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);
});