Commit Graph

22 Commits

Author SHA1 Message Date
Yunfei He 900e4176f9 fix(cli): guard reload against shutdown to prevent orphan child (#183)
A vue-tui:request-reload arriving during the Ctrl+C shutdown window
spawned an orphan child the parent never kills. The reload path and the
shutdown path did not compose: handleReloadRequest only consulted the 3s
startup gate, never isShuttingDown, so a reload accepted during the
pm.shutdown() teardown window ran extractBundle then pm.restart() — which
schedules a fresh 100ms restartTimer. pm.shutdown() had already cleared
its own restartTimer at entry, so nothing cancels the new one; it fires
doSpawn() and creates a brand-new child after the parent has exited.

Fix mirrors respawnTick's existing two-check guard:
- Fast gate: compose shouldAccept as
  `() => acceptReloads && !shutdown.isShuttingDown()` so reloads are
  ignored once Ctrl+C teardown begins.
- Post-await guard (load-bearing): thread isShuttingDown into
  ReloadRequestDeps and re-check it after `await extractBundle` and
  before pm.setBundlePath/pm.restart, covering the case where shutdown
  starts mid-extraction.

The hot handler registration is moved after createShutdown() so the
forward reference resolves (closures read at event time regardless).
Scope is only this reload-vs-shutdown guard; respawnTick, createShutdown
internals, process-manager.ts, and bundle-extractor.ts are unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:44:15 +08:00
Yunfei He 8b10770dc8 fix(cli): clear respawn interval and guard shutdown re-entrancy (#181)
The dev-server shutdown path had two teardown bugs:

1. The crash-respawn setInterval was never cleared. During shutdown's
   async window (await pm.shutdown(); await server.close()), the 500ms
   interval kept firing; if the child had crashed it could call
   pm.spawn() and start a BRAND-NEW child while the parent was tearing
   down — orphaning that child when the parent exits.

2. shutdown was registered directly as the SIGINT/SIGTERM handler with
   no re-entrancy guard. Pressing Ctrl+C twice (common when shutdown
   feels slow) invoked shutdown() twice concurrently → double
   pm.shutdown()/server.close() and two racing process.exit(0).

Fix: extract a testable createShutdown(deps) factory that owns a
shuttingDown flag (2nd+ invocation is a no-op) and clears the respawn
interval FIRST, before the async teardown window. The respawn-tick body
is extracted into respawnTick(deps), which re-checks isShuttingDown
both before and AFTER the extractBundle await — closing the in-flight
window where a tick already mid-extraction could still spawn after
clearInterval. Both helpers are module-scoped (not re-exported from the
package public index.ts).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 22:13:54 +08:00
Yunfei He 41cd720f93 fix(cli): guard request-reload handler against extractBundle rejection (#175)
The vue-tui:request-reload hot handler awaited extractBundle with no
try/catch. extractBundle throws ("No JS bundle found in Vite memoryFiles")
on a transient/broken build, and Vite's hot event emitter does not catch
async handler rejections — so the rejection escaped as an unhandledRejection
that could take down the whole dev process. The crash-respawn interval right
below already guards extractBundle for the same reason.

Extract the reload logic into a testable, module-scoped handleReloadRequest()
(also folding in the acceptReloads startup guard via a shouldAccept predicate)
and wrap the extract/restart work in try/catch: on failure, log via
logger.error and KEEP the previous bundle (no setBundlePath/restart), so a
momentarily broken build no longer kills the dev server. The inline
server.hot.on handler now delegates to it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:47:33 +08:00
Yunfei He 43eda7aa77 fix(cli): serialize extractBundle to prevent concurrent outDir race (#174)
* fix(cli): serialize extractBundle to prevent concurrent outDir race

extractBundle() wipes (rm) then repopulates (mkdir + writeFile) the shared
outDir in place, and dev.ts calls it from two unsynchronized sources — the
vue-tui:request-reload hot handler and the 500ms crash-respawn interval. When
two calls overlap, a second call's rm() can delete the tree a first call is
mid-writeFile into (ENOENT/EINVAL/ENOTEMPTY), or leave a torn, partially
populated dir that the child then loads.

Serialize extractions through a single module-level in-flight promise chain so
concurrent callers queue instead of racing the shared outDir. The internal
chain swallows errors so one failed run can't permanently wedge the queue,
while each caller still receives the real result/rejection. dev.ts is left
untouched; the external contract (args, output layout) is unchanged.

Adds a test that reproduces the race with staggered concurrent calls: red on
origin/main (rejects with EINVAL/ENOENT/ENOTEMPTY), green with the fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(cli): slim extractBundle race test to fit CI timeout

The race test timed out at 5042ms on the 4-core ubuntu CI runner (vitest's
default 5s testTimeout) — it wrote 200 files x ~8KB across 20 staggered
concurrent calls, ~4000 serial ~8KB writes once extractBundle is serialized.

The race window is driven by the NUMBER of files in the write loop and the
staggered starts (more chances for an overlapping rm to interleave a
writeFile), not by file size. So shrink each file's payload from 8KB to 32
bytes to slash write time, keep FILE_COUNT=200, and trim CALLS 20->16 (the
minimum that still reds reliably). Also add an explicit 30s per-test timeout
for comfortable CI headroom.

Verified: green 5x consecutively (~410ms test time locally, ~1.4% of the
timeout); still red 3x on origin/main with the race signature
(EINVAL/ENOENT/ENOTEMPTY). Assertions unchanged in spirit (none reject; final
dir holds the complete file set). Production code untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:35:41 +08:00
Yunfei He dd1194eb73 fix(cli): send SIGTERM before SIGKILL in shutdown() (#170)
shutdown() force-killed the dev child with SIGKILL after a 2000ms wait but
never asked it to stop first. The dev (parent) process receives SIGINT/SIGTERM
directly, but the child is a plain spawn with no shared signal, so it never saw
the parent's signal — waitForExit blocked the full 2000ms, then SIGKILL
(uncatchable) skipped the child runtime's teardown (restore cursor, leave the
alternate screen, disable kitty keyboard, restore raw mode). Result: a 2s hang
on every clean shutdown plus a corrupted terminal afterward.

Send child.kill("SIGTERM") before awaiting exit, mirroring restart(), so a
well-behaved child exits gracefully and SIGKILL only escalates on a hang.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 19:07:40 +08:00
Yunfei He ddd651b5e0 chore: bump all packages to 0.0.3 (#148)
Bump @vue-tui/runtime, @vue-tui/cli, and @vue-tui/testing from 0.0.2 to 0.0.3.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 12:59:34 +08:00
Yunfei He 6d856f8913 build(ci): wire @vue-tui/cli test branch into the ci graph
Add a "test" script (vp test --passWithNoTests) to @vue-tui/cli and a
ci:test:cli branch (dependsOn ci:build) to the run.tasks graph. No CLI tests
exist yet, but wiring the branch now means future CLI tests are covered
automatically rather than silently skipped. #26 (item 3).
2026-05-29 20:00:36 +08:00
Yunfei He 1d6c47ca88 feat(scripts): add check:type running real tsc per package
Each package gets check:type = `tsc --noEmit` (under its own tsconfig); root
check:type = `vp run -r check:type` fans out across the workspace. Unlike vp's
tsgolint-based type-aware path, this is the standard TypeScript compiler, so it
honors each tsconfig's real project semantics.

Additive only here — vp lint still carries typeCheck; the switch-over and ready
rewiring land in the next commit. Note: testing and runtime-tests resolve
@vue-tui/runtime types from its built dist, so check:type requires a prior
build.
2026-05-29 16:54:13 +08:00
Yunfei He 0f60da7ec5 build(cli): add tsconfig and typescript dev dep for type-checking
The cli package had no tsconfig, so it was never type-checked under its own
config. Add a tsconfig (matching the other packages, minus JSX which cli does
not use) and the typescript dev dep so `tsc --noEmit` can run here — a
prerequisite for the upcoming check:type script.
2026-05-29 16:54:13 +08:00
Yunfei He 1f871e1ba5 chore: bump all packages to 0.0.2
Bump @vue-tui/runtime, @vue-tui/cli, and @vue-tui/testing from 0.0.1 to 0.0.2.
2026-05-28 01:10:20 +08:00
Yunfei He 8209f8440b docs: write/optimize READMEs for all published packages
- @vue-tui/runtime: rewrite with pitch, badges, SFC+shallowRef example,
  accurate component/composable tables, app lifecycle examples
- @vue-tui/cli: new README documenting vue-tui dev, HMR, crash recovery
- @vue-tui/testing: rewrite with @testing-library positioning, correct
  async render API, cleanup globals:true requirement
- Add early-stage warning to all sub-packages (matching root README)
- Fix CLI package.json: add license field, correct description

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:08:48 +08:00
Yunfei He 72cf46987c chore: optimize package.json descriptions for npm discoverability
Improve descriptions across all packages to include key differentiators
(Yoga flexbox, AI agent, React Ink lineage) and rename workspace root
from vue-tui to vue-tui-monorepo to avoid confusion with the taken npm name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:39:24 +08:00
Yunfei He 3565c5dfb5 fix: clean up ChildProcess type workaround in CLI
Replace ugly `as unknown as EventEmitter` casts with a proper
type alias `Process = ChildProcess & NodeJS.EventEmitter`.
@types/node@25 removed .on() from ChildProcess class declaration
even though it extends EventEmitter at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:34:49 +08:00
Yunfei He 6e01954cf9 fix: resolve vpr ready failures
- Fix unhandled promise in coding-agent example (void operator)
- Fix unhandled promise in cli/dev.ts (void operator)
- Fix ChildProcess.on() type error in cli/process-manager.ts
  (@types/node v25 removed .on() from ChildProcess type)
- Change example build scripts from "vite build" to "vp build"
  (vite resolves to vite-plus-core which has no CLI bin)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:19:55 +08:00
Yunfei He 15cf216291 fix(cli): add prepublishOnly to ensure build before publish
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:04:56 +08:00
Yunfei He ae33947c37 chore: prepare v0.0.1 release
- Bump all packages to 0.0.1
- Add publishConfig.access: public to @vue-tui/cli
- Disable tsgo for dts generation (missing native-preview dep)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:58:29 +08:00
Yunfei He 625aa138f1 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>
2026-05-24 21:39:28 +08:00
Yunfei He d31e8b7a65 fix(cli): clear screen before spawning child process
Prevents Vite startup logs and connection messages from mixing
with the TUI app's output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:23:26 +08:00
Yunfei He 020fd0d85a feat: add basic-jsx and basic-template examples
- basic-jsx: Counter + Clock with defineComponent() + JSX
- basic-template: Counter + Clock with .vue SFC + <template>
- Both demonstrate HMR via vue-tui dev
- Fix vite-plugin: strip build.lib and external config for bundledDev,
  add node builtins resolveId to prevent browser-external shimming
- Remove old hmr-demo example (superseded by basic-template)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:21:08 +08:00
Yunfei He e8ef31814a feat: add hmr-demo example with Vue SFC HMR
- Counter + Clock components demonstrating component-level HMR
- Fix hmr-loader: use project-root file:// URL prefix so Node's
  bare specifier resolution finds node_modules
- Fix hmr-loader: export hooks directly instead of inline data URL
- Fix process-manager: register loader via data URL + register()
  pointing to the file (matches spike pattern)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:48:52 +08:00
Yunfei He d319e77540 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>
2026-05-24 19:57:47 +08:00
Yunfei He 2b7d1250f4 feat(cli): scaffold @vue-tui/cli package with all modules
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 19:55:46 +08:00