feat(cli): scaffold @vue-tui/cli package with all modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-24 19:55:46 +08:00
parent fda19b926d
commit 2b7d1250f4
9 changed files with 245 additions and 0 deletions
+26
View File
@@ -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:"
}
}
+35
View File
@@ -0,0 +1,35 @@
import { mkdir, rm, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
export interface MemoryFiles {
files: Map<string, unknown>;
get(key: string): { source: string | Uint8Array } | undefined;
}
export async function extractBundle(memoryFiles: MemoryFiles, outDir: string): Promise<string> {
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;
}
+25
View File
@@ -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,
);
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env node
const entry = process.argv[2];
console.log(`vue-tui dev ${entry ?? "(auto-detect)"} — not yet implemented`);
+25
View File
@@ -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");
},
};
}
+82
View File
@@ -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<void> {
return new Promise((resolve) => {
const timer = setTimeout(() => {
resolve();
}, timeout);
proc.on("exit", () => {
clearTimeout(timer);
resolve();
});
});
}
let restartTimer: ReturnType<typeof setTimeout> | 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;
},
};
}
+29
View File
@@ -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,
},
};
},
};
}
+9
View File
@@ -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,
},
});