Skip to content

Add WebSocket-based live reload for development#86

Merged
antosubash merged 7 commits intomainfrom
claude/implement-hot-reload-potw4
Apr 5, 2026
Merged

Add WebSocket-based live reload for development#86
antosubash merged 7 commits intomainfrom
claude/implement-hot-reload-potw4

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Implement automatic browser refresh when Vite builds or Tailwind CSS
compilation completes during development. This eliminates the need
to manually refresh the browser after editing frontend code.

Key changes:

  • LiveReloadServer: WebSocket hub that broadcasts reload signals to
    connected browsers at /dev/live-reload
  • ViteDevWatchService: now notifies LiveReloadServer after successful
    builds, with CSS-only vs full reload distinction
  • HtmlFileInertiaPageRenderer: injects a lightweight client script in
    dev mode that connects to the WebSocket and auto-reloads (CSS-only
    swap for Tailwind changes, full reload for JS/TSX)
  • CSP updated to allow WebSocket connections in development
  • Reconnection with exponential backoff (1s to 10s max)

claude and others added 7 commits April 5, 2026 14:09
Implement automatic browser refresh when Vite builds or Tailwind CSS
compilation completes during development. This eliminates the need
to manually refresh the browser after editing frontend code.

Key changes:
- LiveReloadServer: WebSocket hub that broadcasts reload signals to
  connected browsers at /dev/live-reload
- ViteDevWatchService: now notifies LiveReloadServer after successful
  builds, with CSS-only vs full reload distinction
- HtmlFileInertiaPageRenderer: injects a lightweight client script in
  dev mode that connects to the WebSocket and auto-reloads (CSS-only
  swap for Tailwind changes, full reload for JS/TSX)
- CSP updated to allow WebSocket connections in development
- Reconnection with exponential backoff (1s to 10s max)
Replace the build-and-refresh workflow with native Vite dev server HMR
and .NET hot reload for a proper development experience:

Vite dev server integration:
- vite.dev.config.ts: Dev server config with React Fast Refresh,
  @tailwindcss/vite for CSS HMR, and module page resolution plugin
- vite-plugin-module-hmr.ts: Resolves /_content/ module page imports
  to actual source files so Vite can serve them with HMR
- ViteDevMiddleware: ASP.NET middleware that detects and proxies
  requests to Vite dev server (/@vite/, /@fs/, .tsx files)
- HtmlFileInertiaPageRenderer: In Vite dev mode, strips import map,
  removes pre-built CSS link, and injects Vite HMR client scripts

sm dev CLI command:
- Runs dotnet watch (C# hot reload) + Vite dev server (frontend HMR)
- Supports --no-vite, --no-dotnet, --vite-port flags
- Graceful shutdown of all processes on Ctrl+C

The LiveReload WebSocket fallback is preserved for running plain
dotnet run without the Vite dev server.
DevCommand (sm dev):
- Graceful shutdown: SIGTERM first, wait 5s, then SIGKILL
- Second Ctrl+C force-kills immediately instead of being swallowed
- Unix: kill process group (-PID) so shell-spawned children get the signal
- Windows: taskkill /T for process tree termination
- Log PIDs on start and warn about survivors on failed shutdown
- Dispose all Process handles on exit
- Unregister event handlers to prevent accumulation
- ProcessExit handler force-kills to prevent orphans on unexpected exit

dev-orchestrator.mjs (npm run dev):
- Graceful SIGTERM then SIGKILL after 3s timeout
- Unix: process.kill(-pid, 'SIGTERM') for process group cleanup
- Windows: taskkill /T /F for process tree cleanup
- Handle SIGHUP (terminal close)
- process.on('exit') safety net force-kills all children
- uncaughtException handler kills children before crashing
- Track labels with processes for better diagnostics
DevCommand (sm dev) — C#:
- Replace broken `kill -TERM -<pid>` (process group kill) with proper
  descendant tree walk: /proc/*/stat on Linux, pgrep -P on macOS
- Send SIGTERM leaf-first so parents don't respawn before we reach them
- Force-kill fallback: Kill(entireProcessTree: true) which uses
  /proc on Linux, libproc on macOS, NtQuerySystemInformation on Windows
- If tree kill fails (permission denied on a child), fall back to
  killing just the direct process
- Windows: taskkill /T (no /F) for graceful, Kill(tree) for force

dev-orchestrator.mjs — Node:
- Use detached: true on Unix so each child becomes a process group
  leader, making process.kill(-pid) actually work
- Remove shell: true — it created intermediary sh processes that
  swallowed signals and made proc.pid point to the shell, not the
  actual dotnet/node process
- Resolve npx binary directly instead of going through shell
- Guard SIGHUP handler behind platform check (doesn't exist on Windows)
- Fix 'exit' handler: use process.kill() (sync) not spawn() (async)
  since exit callbacks can't do async work
- Handle ESRCH (already exited) gracefully instead of logging warnings
Before launching dotnet watch or Vite dev server, both sm dev and
npm run dev now check if the required ports are free. If a port is
occupied, the user sees what process holds it and can choose to kill
it interactively.

PortChecker (C#):
- Linux: ss -tlnp (fast) with lsof fallback
- macOS: lsof -iTCP:PORT -sTCP:LISTEN
- Windows: netstat -ano + tasklist for process name
- Parses launchSettings.json to discover ASP.NET ports dynamically
- Kill uses Process.Kill(entireProcessTree: true) then verifies release

dev-orchestrator.mjs (Node):
- Same cross-platform port detection (lsof on Unix, netstat on Windows)
- Linux: reads /proc/<pid>/comm for process name
- macOS: ps -p <pid> -o comm= for process name
- Windows: tasklist /FI for process name
- Interactive Y/n prompt before killing
- Parses launchSettings.json for ASP.NET port discovery
Bug fixes:
- ViteEntryScripts: add CSP nonce to injected <script> tags — they
  were being blocked by the Content-Security-Policy header
- ViteDevMiddleware: fix race condition on _lastCheck/_viteDetected
  using Interlocked.CompareExchange to prevent thundering-herd probes

Code quality:
- Extract DevToolsConstants.ViteDevServerKey shared between middleware
  and renderer — eliminates stringly-typed HttpContext.Items coupling
- Replace magic-number _shutdownState (0/1/2) with ShutdownPhase
  constants for readability
- Pass environment variables via parameter to StartProcess instead of
  checking label == "dotnet" string inside the method
- Merge duplicate ShouldProxy/IsSourceFileRequest branches into single
  condition in ViteDevMiddleware
- Simplify RenderPageAsync from 3-way if/else to direct expressions
- Move mid-file ESM imports to top of dev-orchestrator.mjs

Efficiency:
- LiveReloadServer.NotifyReloadAsync: send to all WebSocket clients
  concurrently via Task.WhenAll instead of sequentially
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6d859ce
Status: ✅  Deploy successful!
Preview URL: https://652e9c45.simplemodule-website.pages.dev
Branch Preview URL: https://claude-implement-hot-reload.simplemodule-website.pages.dev

View logs

@antosubash antosubash merged commit e9e6016 into main Apr 5, 2026
4 checks passed
@antosubash antosubash deleted the claude/implement-hot-reload-potw4 branch April 5, 2026 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants