feat(dev): pause and resume HMR during file changes#1297
feat(dev): pause and resume HMR during file changes#1297martin-mastermind wants to merge 1 commit intonuxt:mainfrom
Conversation
Add ability to temporarily pause Vite and Nitro HMR when processing multiple file changes, particularly useful when AI agents auto-save edits. Files can be accumulated silently, then reloaded in a single batch when changes are complete. Keyboard shortcut: Press 'p' in the dev terminal to toggle pause. Alternative: Create/delete .hmr-pause file in project root. Changes: - Wrap viteServer.hot.send to buffer HMR messages when paused - Guard load() to queue pending reloads instead of executing immediately - Watch sentinel file (.hmr-pause) to trigger pause/resume state changes - Add stdin keyboard handler for 'p' key to toggle pause state Fixes nuxt#1223
bc5fdbc to
714df2a
Compare
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe change implements a keyboard-controlled pause/resume mechanism for Vite HMR in the Nuxt dev server. In TTY environments, pressing the 'p' key creates or removes a Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/nuxi/src/commands/dev.ts`:
- Around line 90-105: Wrap the stdin setup (process.stdin.setRawMode,
process.stdin.resume, process.stdin.setEncoding, and the data listener that
toggles the '.hmr-pause' sentinel) into a scope where you also create a
cleanupStdin function that removes the 'data' listener and restores raw mode (if
setRawMode exists) and pauses stdin; then call cleanupStdin from every
shutdown/close path in this module (the handlers that currently return without
cleanup) so the terminal state is always restored. Ensure cleanupStdin
references the exact listener function you passed to process.stdin.on('data',
...) so it can be detached, and invoke it before any early return or process
exit.
- Around line 95-103: In the process.stdin.on('data') handler (the key === 'p'
branch) avoid the check-then-act race by removing the existsSync + rmSync
pattern and call rmSync(sentinelPath, { force: true }) to delete the sentinel
safely (or wrap the whole branch in a try-catch); keep the
writeFileSync(sentinelPath, '') for the create case, and ensure you update the
logic around sentinelPath/resolve so the deletion uses rmSync with the force
option to prevent ENOENT from crashing the dev server.
In `@packages/nuxi/src/dev/utils.ts`:
- Around line 245-249: closeWatchers currently closes `#sentinelWatcher` and
`#distWatcher` but calls `#configWatcher` as a function; ensure you call .close() on
watchers (use this.#configWatcher?.close()) and avoid permanently removing the
sentinel watcher: modify load() to re-register the `.hmr-pause` sentinel watcher
after a reload (or move sentinel watcher setup into a helper that's invoked both
on initial start and inside load()), and ensure closeWatchers only closes
watchers (this.#sentinelWatcher?.close(); this.#distWatcher?.close();
this.#configWatcher?.close()) so that the sentinel can be re-created by the
load() re-registration logic (affects closeWatchers and the load() watcher
setup).
- Around line 345-347: The fs.watch callback currently returns early when
filename is null/undefined due to the strict check in the watcher assigned to
this.#sentinelWatcher; update the callback used with watch so it only compares
filename when defined (e.g., if (filename && filename !== '.hmr-pause') return)
or remove the filename guard so the sentinelPath existence check always runs;
ensure the logic that calls existsSync(sentinelPath) then calls this.pause() or
this.resume() remains intact and referenced from the same watcher callback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: eb9d9955-9ed6-4c92-a652-0fd96d20b7d7
📒 Files selected for processing (2)
packages/nuxi/src/commands/dev.tspackages/nuxi/src/dev/utils.ts
| if (key === 'p') { | ||
| const sentinelPath = resolve(cwd, '.hmr-pause') | ||
| if (existsSync(sentinelPath)) { | ||
| rmSync(sentinelPath) | ||
| } | ||
| else { | ||
| writeFileSync(sentinelPath, '') | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Locate and examine the dev.ts file
fd -t f "dev.ts" packages/nuxi/src/commands/Repository: nuxt/cli
Length of output: 86
🏁 Script executed:
# Read the specific lines and surrounding context
cat -n packages/nuxi/src/commands/dev.ts | sed -n '85,115p'Repository: nuxt/cli
Length of output: 1053
🏁 Script executed:
# Check file size to determine read strategy
wc -l packages/nuxi/src/commands/dev.tsRepository: nuxt/cli
Length of output: 90
🏁 Script executed:
# Check the full context of the function and look for error handlers
cat -n packages/nuxi/src/commands/dev.ts | sed -n '1,120p'Repository: nuxt/cli
Length of output: 4343
🏁 Script executed:
# Search for try-catch blocks or error handlers in this file
rg -n "try\s*{|catch\s*\(|process\.stdin\.on\(" packages/nuxi/src/commands/dev.tsRepository: nuxt/cli
Length of output: 181
🏁 Script executed:
# Check if rmSync/writeFileSync are imported and from which module
head -50 packages/nuxi/src/commands/dev.ts | grep -E "^import|^from"Repository: nuxt/cli
Length of output: 752
🏁 Script executed:
# Verify Node.js rmSync behavior - check if it can throw
node <<'EOF'
const { rmSync } = require('fs');
try {
rmSync('/nonexistent/path/file.txt');
} catch (e) {
console.log(`rmSync throws: ${e.code}`);
}
EOFRepository: nuxt/cli
Length of output: 74
🏁 Script executed:
# Search for other stdin event handlers in the codebase to see if they use error handling
rg -n "stdin\.on\(" packages/nuxi/src/ -A 5Repository: nuxt/cli
Length of output: 496
🏁 Script executed:
# Check how errors in process.stdin event handlers are typically handled
node <<'EOF'
const fs = require('fs');
// Simulate what happens if an error is thrown in stdin handler
if (process.stdin.isTTY) {
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.setEncoding('utf8');
// Set up error handler for stdin
process.stdin.on('error', (err) => {
console.log('stdin error handler caught:', err.message);
});
process.stdin.on('data', (key) => {
console.log('Received key:', key);
// Simulate the race condition - throw an error
if (key === 'p') {
throw new Error('ENOENT: no such file or directory');
}
});
console.log('Setup complete - stdin error handling demonstrated');
} else {
console.log('Not a TTY, skipping test');
}
EOFRepository: nuxt/cli
Length of output: 77
🏁 Script executed:
# Check if process has any error handling or signal handlers for uncaught exceptions in event handlers
tail -100 packages/nuxi/src/commands/dev.ts | head -80Repository: nuxt/cli
Length of output: 2269
🏁 Script executed:
# Check Node.js rmSync documentation and options
node <<'EOF'
const { rmSync } = require('fs');
const fs = require('fs');
// Create a test file
fs.writeFileSync('/tmp/test-file.txt', 'test');
// Test rmSync with force option
try {
rmSync('/tmp/test-file.txt', { force: true });
console.log('rmSync with force: true succeeded');
} catch (e) {
console.log('rmSync with force threw:', e.code);
}
// Test rmSync without force on non-existent file
try {
rmSync('/tmp/nonexistent.txt', { force: true });
console.log('rmSync force: true on nonexistent file succeeded');
} catch (e) {
console.log('rmSync threw:', e.code);
}
EOFRepository: nuxt/cli
Length of output: 135
🏁 Script executed:
# Check if there are other similar file operation patterns in the codebase that handle errors
rg -n "rmSync|writeFileSync" packages/nuxi/src/ -B 2 -A 2 | head -40Repository: nuxt/cli
Length of output: 2522
🏁 Script executed:
# Verify what happens with uncaught exceptions in process.stdin event handlers
# Check if the dev command has any process-level error handlers
rg -n "process.on|uncaughtException|unhandledRejection" packages/nuxi/src/commands/dev.tsRepository: nuxt/cli
Length of output: 34
🏁 Script executed:
# Check if there's a wrapper or CLI error handler that might catch these
cat -n packages/nuxi/src/commands/dev.ts | head -30Repository: nuxt/cli
Length of output: 1278
Use exception-safe file operations in the stdin event handler.
Lines 96-102 use a check-then-act pattern (existsSync + rmSync) that can throw ENOENT if the file is deleted between the check and deletion. Since this code runs inside a process.stdin.on('data') event handler with no error boundary, an uncaught exception will crash the dev server. Replace with rmSync(sentinelPath, { force: true }) to safely handle both the race condition and non-existent files, or wrap the operations in a try-catch block.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/nuxi/src/commands/dev.ts` around lines 95 - 103, In the
process.stdin.on('data') handler (the key === 'p' branch) avoid the
check-then-act race by removing the existsSync + rmSync pattern and call
rmSync(sentinelPath, { force: true }) to delete the sentinel safely (or wrap the
whole branch in a try-catch); keep the writeFileSync(sentinelPath, '') for the
create case, and ensure you update the logic around sentinelPath/resolve so the
deletion uses rmSync with the force option to prevent ENOENT from crashing the
dev server.
| closeWatchers(): void { | ||
| this.#sentinelWatcher?.close() | ||
| this.#distWatcher?.close() | ||
| this.#configWatcher?.() | ||
| } |
There was a problem hiding this comment.
Re-register the .hmr-pause watcher after reload.
Line 246 closes #sentinelWatcher, but load() only reattaches config watching (Line 272). After first reload, sentinel toggling no longer works.
Proposed fix
@@
}
- this.#watchConfig()
+ this.#watchConfig()
+ this.#watchSentinel()
}Also applies to: 258-273
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/nuxi/src/dev/utils.ts` around lines 245 - 249, closeWatchers
currently closes `#sentinelWatcher` and `#distWatcher` but calls `#configWatcher` as a
function; ensure you call .close() on watchers (use
this.#configWatcher?.close()) and avoid permanently removing the sentinel
watcher: modify load() to re-register the `.hmr-pause` sentinel watcher after a
reload (or move sentinel watcher setup into a helper that's invoked both on
initial start and inside load()), and ensure closeWatchers only closes watchers
(this.#sentinelWatcher?.close(); this.#distWatcher?.close();
this.#configWatcher?.close()) so that the sentinel can be re-created by the
load() re-registration logic (affects closeWatchers and the load() watcher
setup).
🔗 Linked issue
Fixes #1223
📚 Description
Add ability to temporarily pause Vite and Nitro HMR when processing multiple file changes, particularly useful when AI agents auto-save edits. Files can be accumulated silently, then reloaded in a single batch when changes are complete.
Keyboard shortcut: Press 'p' in the dev terminal to toggle pause.
Alternative: Create/delete .hmr-pause file in project root.
Changes: