From 2b7d1250f42bec29c5823b9d518f24daaa51dd4b Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Sun, 24 May 2026 19:55:46 +0800 Subject: [PATCH] feat(cli): scaffold @vue-tui/cli package with all modules Co-Authored-By: Claude Sonnet 4.6 --- packages/cli/package.json | 26 +++++++++ packages/cli/src/bundle-extractor.ts | 35 ++++++++++++ packages/cli/src/hmr-loader.ts | 25 +++++++++ packages/cli/src/index.ts | 4 ++ packages/cli/src/logger.ts | 25 +++++++++ packages/cli/src/process-manager.ts | 82 ++++++++++++++++++++++++++++ packages/cli/src/vite-plugin.ts | 29 ++++++++++ packages/cli/vite.config.ts | 9 +++ pnpm-lock.yaml | 10 ++++ 9 files changed, 245 insertions(+) create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/bundle-extractor.ts create mode 100644 packages/cli/src/hmr-loader.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/logger.ts create mode 100644 packages/cli/src/process-manager.ts create mode 100644 packages/cli/src/vite-plugin.ts create mode 100644 packages/cli/vite.config.ts diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..0a0dcb0 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,26 @@ +{ + "name": "@vue-tui/cli", + "version": "0.0.0", + "bin": { + "vue-tui": "./dist/index.mjs" + }, + "files": [ + "dist" + ], + "type": "module", + "exports": { + ".": { + "import": "./dist/index.mjs" + } + }, + "scripts": { + "build": "vp pack", + "dev": "vp pack --watch" + }, + "dependencies": { + "vite": "catalog:" + }, + "devDependencies": { + "vite-plus": "catalog:" + } +} diff --git a/packages/cli/src/bundle-extractor.ts b/packages/cli/src/bundle-extractor.ts new file mode 100644 index 0000000..8efc98d --- /dev/null +++ b/packages/cli/src/bundle-extractor.ts @@ -0,0 +1,35 @@ +import { mkdir, rm, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; + +export interface MemoryFiles { + files: Map; + get(key: string): { source: string | Uint8Array } | undefined; +} + +export async function extractBundle(memoryFiles: MemoryFiles, outDir: string): Promise { + await rm(outDir, { recursive: true, force: true }); + await mkdir(outDir, { recursive: true }); + + const keys = [...memoryFiles.files.keys()]; + let entryPath: string | undefined; + + for (const key of keys) { + const file = memoryFiles.get(key); + if (!file) continue; + + const outPath = join(outDir, key); + await mkdir(dirname(outPath), { recursive: true }); + + const source = typeof file.source === "string" ? file.source : Buffer.from(file.source); + await writeFile(outPath, source); + + if (key.endsWith(".js") && !key.endsWith(".js.map")) { + entryPath ??= outPath; + } + } + + if (!entryPath) { + throw new Error("No JS bundle found in Vite memoryFiles"); + } + return entryPath; +} diff --git a/packages/cli/src/hmr-loader.ts b/packages/cli/src/hmr-loader.ts new file mode 100644 index 0000000..238aa97 --- /dev/null +++ b/packages/cli/src/hmr-loader.ts @@ -0,0 +1,25 @@ +import { register } from "node:module"; + +register( + `data:text/javascript,${encodeURIComponent(` +const VITE_PORT = process.env.VUE_TUI_HMR_PORT || '5173'; + +export async function resolve(specifier, context, nextResolve) { + if (/^\\/hmr_patch_\\d+\\.js$/.test(specifier)) { + return { url: 'vite-hmr://' + specifier, shortCircuit: true }; + } + return nextResolve(specifier, context); +} + +export async function load(url, context, nextLoad) { + if (url.startsWith('vite-hmr://')) { + const path = url.replace('vite-hmr://', ''); + const res = await fetch('http://localhost:' + VITE_PORT + path); + if (!res.ok) throw new Error('HMR patch fetch failed: ' + res.status); + return { format: 'module', source: await res.text(), shortCircuit: true }; + } + return nextLoad(url, context); +} +`)}`, + import.meta.url, +); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 0000000..09dd62a --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env node + +const entry = process.argv[2]; +console.log(`vue-tui dev ${entry ?? "(auto-detect)"} — not yet implemented`); diff --git a/packages/cli/src/logger.ts b/packages/cli/src/logger.ts new file mode 100644 index 0000000..984e698 --- /dev/null +++ b/packages/cli/src/logger.ts @@ -0,0 +1,25 @@ +export type LogMode = "stdout" | "silent"; + +export interface Logger { + mode: LogMode; + info(msg: string): void; + error(msg: string): void; +} + +export function createLogger(): Logger { + let mode: LogMode = "stdout"; + return { + get mode() { + return mode; + }, + set mode(m: LogMode) { + mode = m; + }, + info(msg: string) { + if (mode === "stdout") process.stdout.write(msg + "\n"); + }, + error(msg: string) { + if (mode === "stdout") process.stderr.write(msg + "\n"); + }, + }; +} diff --git a/packages/cli/src/process-manager.ts b/packages/cli/src/process-manager.ts new file mode 100644 index 0000000..c897540 --- /dev/null +++ b/packages/cli/src/process-manager.ts @@ -0,0 +1,82 @@ +import { spawn, type ChildProcess } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import type { Logger } from "./logger.ts"; + +export interface ProcessManagerOptions { + bundlePath: string; + hmrPort: number; + logger: Logger; + onExit?: (code: number | null) => void; +} + +const loaderPath = fileURLToPath(new URL("./hmr-loader.mjs", import.meta.url)); + +export function createProcessManager(options: ProcessManagerOptions) { + let child: ChildProcess | null = null; + let currentBundlePath = options.bundlePath; + + function doSpawn() { + options.logger.mode = "silent"; + child = spawn("node", ["--import", loaderPath, currentBundlePath], { + stdio: "inherit", + env: { + ...process.env, + VUE_TUI_DEV: "1", + VUE_TUI_HMR_PORT: String(options.hmrPort), + }, + }); + + child.on("exit", (code) => { + options.logger.mode = "stdout"; + child = null; + options.onExit?.(code); + }); + } + + function waitForExit(proc: ChildProcess, timeout: number): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => { + resolve(); + }, timeout); + proc.on("exit", () => { + clearTimeout(timer); + resolve(); + }); + }); + } + + let restartTimer: ReturnType | null = null; + + return { + spawn: doSpawn, + + setBundlePath(path: string) { + currentBundlePath = path; + }, + + async restart() { + if (restartTimer) clearTimeout(restartTimer); + restartTimer = setTimeout(async () => { + restartTimer = null; + if (child) { + child.kill("SIGTERM"); + await waitForExit(child, 2000); + if (child) child.kill("SIGKILL"); + } + doSpawn(); + }, 100); + }, + + async shutdown() { + if (restartTimer) clearTimeout(restartTimer); + if (child) { + await waitForExit(child, 2000); + if (child) child.kill("SIGKILL"); + } + }, + + get running() { + return child !== null; + }, + }; +} diff --git a/packages/cli/src/vite-plugin.ts b/packages/cli/src/vite-plugin.ts new file mode 100644 index 0000000..e7a09ff --- /dev/null +++ b/packages/cli/src/vite-plugin.ts @@ -0,0 +1,29 @@ +import type { Plugin } from "vite"; + +export interface VueTuiDevPluginOptions { + entry?: string; +} + +export function vueTuiDevPlugin(options?: VueTuiDevPluginOptions): Plugin { + return { + name: "vue-tui:dev", + config() { + return { + experimental: { bundledDev: true }, + build: { + modulePreload: false, + ...(options?.entry ? { lib: { entry: options.entry } } : {}), + }, + define: { + __VUE_TUI_DEV__: "true", + }, + resolve: { + conditions: ["node"], + }, + server: { + strictPort: true, + }, + }; + }, + }; +} diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts new file mode 100644 index 0000000..5150e9f --- /dev/null +++ b/packages/cli/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + pack: { + entry: ["src/index.ts", "src/hmr-loader.ts"], + format: "esm", + shims: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a490316..23a16c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,16 @@ importers: specifier: npm:@voidzero-dev/vite-plus-core@latest version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' + packages/cli: + dependencies: + vite: + specifier: npm:@voidzero-dev/vite-plus-core@latest + version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.8.0)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0)' + devDependencies: + vite-plus: + specifier: 'catalog:' + version: 0.1.22(@types/node@25.8.0)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.8.0)(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0))(jiti@2.7.0)(typescript@6.0.3)(yaml@2.9.0) + packages/runtime: dependencies: '@vue/runtime-core':