fix(cli): clean terminal output for TUI apps

- Silence Vite server logs (logLevel: 'silent')
- Intercept console.log/info/warn/error/debug in child process to
  suppress [vite] and [Vue warn] noise from HMR client
- Add 3s startup grace period to ignore initial reload requests
  (prevents double-spawn on first WS connection)
- Clear screen before spawning child process

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yunfei He
2026-05-24 21:28:29 +08:00
parent d31e8b7a65
commit 625aa138f1
9 changed files with 25 additions and 132 deletions
-20
View File
@@ -1,20 +0,0 @@
{
"name": "@vue-tui/example-flappy-bird",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"start": "vite build && node dist/game.mjs"
},
"dependencies": {
"@vue-tui/runtime": "workspace:*",
"chalk": "^5.3.0",
"vue": "^3.4.0"
},
"devDependencies": {
"@types/node": "catalog:",
"@vitejs/plugin-vue": "^6",
"vite": "catalog:"
}
}
-31
View File
@@ -1,31 +0,0 @@
<script setup lang="ts">
import { shallowRef } from "vue";
import { Box, Text, useFocusManager, useInput } from "@vue-tui/runtime";
import Item from "./Item.vue";
const items = ["apple", "banana", "orange", "grape", "watermelon"];
const selected = shallowRef<null | string>(null);
const focusManager = useFocusManager();
useInput((input, key) => {
if (key.upArrow) {
focusManager.focusPrevious();
} else if (key.downArrow) {
focusManager.focusNext();
} else if (key.return) {
console.log("Selected:", focusManager.activeId);
selected.value = focusManager.activeId;
}
if (input === "q") {
process.exit(0);
}
});
</script>
<template>
<Box>
<Item v-for="item in items" :key="item" :id="item" :label="item" />
<Text>You selected: {{ focusManager.activeId }}</Text>
</Box>
</template>
-17
View File
@@ -1,17 +0,0 @@
<script setup lang="ts">
import { Box, Text, useFocus } from "@vue-tui/runtime";
const props = defineProps<{
id: string;
label: string;
}>();
const focusCtx = useFocus({ id: props.id });
</script>
<template>
<Box>
<Text :color="focusCtx.isFocused.value ? 'cyan' : 'undefined'"
>{{ focusCtx.isFocused.value ? ">" : " " }} {{ props.label }}</Text
>
</Box>
</template>
-10
View File
@@ -1,10 +0,0 @@
// Flappy Bird example for @vue-tui/runtime.
//
// pnpm --filter @vue-tui/example-flappy-bird start
//
// Controls: space / ↑ / w to flap, q or Ctrl-C to quit, r to restart after dying.
import { createApp } from "@vue-tui/runtime";
import App from "./App.vue";
createApp(App).mount();
-6
View File
@@ -1,6 +0,0 @@
declare module "*.vue" {
import type { DefineComponent } from "vue";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const component: DefineComponent<{}, {}, any>;
export default component;
}
-19
View File
@@ -1,19 +0,0 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["es2023"],
"module": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"types": ["node"],
"strict": true,
"noUnusedLocals": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"jsx": "preserve"
},
"include": ["src/**/*.ts", "src/**/*.vue", "vite.config.ts"]
}
-28
View File
@@ -1,28 +0,0 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath } from "node:url";
const here = fileURLToPath(new URL(".", import.meta.url));
// Build the example as a Node ESM bundle. SFCs are compiled by
// @vitejs/plugin-vue; runtime deps (vue, @vue-tui/runtime, chalk, …) stay
// external so Node resolves them from node_modules at startup.
export default defineConfig({
plugins: [vue()],
build: {
target: "node22",
outDir: "dist",
emptyOutDir: true,
minify: false,
lib: {
entry: `${here}src/main.ts`,
formats: ["es"],
fileName: () => "game.mjs",
},
rollupOptions: {
// Bundle anything resolved as a relative / absolute path (the .vue
// file + its `?vue` virtual modules); externalize bare specifiers.
external: (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("\0"),
},
},
});
+9
View File
@@ -17,6 +17,7 @@ export async function dev(entry?: string) {
const server = await createServer({
plugins: [vueTuiDevPlugin({ entry })],
logLevel: "silent",
});
await server.listen();
@@ -55,8 +56,16 @@ export async function dev(entry?: string) {
pm.spawn();
// Ignore reload requests during startup — the initial WS connection
// may trigger vite:beforeFullReload before the app is ready
let acceptReloads = false;
setTimeout(() => {
acceptReloads = true;
}, 3000);
// Listen for full reload requests from child
server.hot.on("vue-tui:request-reload", async () => {
if (!acceptReloads) return;
const newPath = await extractBundle(clientEnv.memoryFiles, outDir);
pm.setBundlePath(newPath);
pm.restart();
+16 -1
View File
@@ -9,7 +9,22 @@ export interface ProcessManagerOptions {
}
const loaderUrl = new URL("./hmr-loader.mjs", import.meta.url).href;
const loaderBootstrap = `data:text/javascript,import{register}from"node:module";register(${JSON.stringify(loaderUrl)})`;
// Bootstrap script: register HMR loader hooks + silence Vite/Vue console noise
const loaderBootstrap = `data:text/javascript,${encodeURIComponent(`
import{register}from"node:module";
register(${JSON.stringify(loaderUrl)});
// Intercept all console methods to suppress Vite HMR client and Vue warn noise
for (const method of ["log", "info", "warn", "error", "debug"]) {
const orig = console[method];
console[method] = (...args) => {
const first = typeof args[0] === "string" ? args[0] : "";
if (first.includes("[vite]") || first.includes("[Vue warn]")) return;
orig.apply(console, args);
};
}
`)}`;
export function createProcessManager(options: ProcessManagerOptions) {
let child: ChildProcess | null = null;