Skip to content

feat(dev): pause and resume HMR during file changes#1297

Closed
martin-mastermind wants to merge 1 commit intonuxt:mainfrom
martin-mastermind:feat/pause-resume-hmr
Closed

feat(dev): pause and resume HMR during file changes#1297
martin-mastermind wants to merge 1 commit intonuxt:mainfrom
martin-mastermind:feat/pause-resume-hmr

Conversation

@martin-mastermind
Copy link
Copy Markdown
Contributor

🔗 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:

  • 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

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

Warning

Rate limit exceeded

@martin-mastermind has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 55 minutes and 5 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f935f544-8d64-4f91-936b-137fdcf7412b

📥 Commits

Reviewing files that changed from the base of the PR and between bc5fdbc and 714df2a.

📒 Files selected for processing (2)
  • packages/nuxi/src/commands/dev.ts
  • packages/nuxi/src/dev/utils.ts
📝 Walkthrough

Walkthrough

The 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 .hmr-pause sentinel file. The dev server watches this sentinel file; when present, it defers reload operations and accumulates HMR messages before dispatching them. Upon resuming (sentinel removal), pending updates are flushed and queued reloads are executed. Two new public methods (pause(), resume()) and a getter (isPaused()) are exposed on the NuxtDevServer class.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarizes the primary change: adding pause/resume functionality for HMR during file changes, which matches the core objective from issue #1223.
Description check ✅ Passed The description is well-related to the changeset, explaining the purpose (pausing HMR during multiple file changes), the keyboard toggle mechanism ('p' key), the sentinel file approach, and listing the implementation changes made.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #1223: preventing noisy HMR triggers [#1223], providing pause mechanism via keyboard and sentinel file [#1223], and enabling silent accumulation with single reload [#1223].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the HMR pause/resume feature: keyboard input handling in dev.ts and pause/resume logic with state tracking in dev/utils.ts. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 56144b6 and bc5fdbc.

📒 Files selected for processing (2)
  • packages/nuxi/src/commands/dev.ts
  • packages/nuxi/src/dev/utils.ts

Comment thread packages/nuxi/src/commands/dev.ts
Comment on lines +95 to +103
if (key === 'p') {
const sentinelPath = resolve(cwd, '.hmr-pause')
if (existsSync(sentinelPath)) {
rmSync(sentinelPath)
}
else {
writeFileSync(sentinelPath, '')
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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.ts

Repository: 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}`);
}
EOF

Repository: 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 5

Repository: 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');
}
EOF

Repository: 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 -80

Repository: 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);
}
EOF

Repository: 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 -40

Repository: 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.ts

Repository: 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 -30

Repository: 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.

Comment on lines 245 to 249
closeWatchers(): void {
this.#sentinelWatcher?.close()
this.#distWatcher?.close()
this.#configWatcher?.()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment thread packages/nuxi/src/dev/utils.ts Outdated
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 27, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing martin-mastermind:feat/pause-resume-hmr (bc5fdbc) with main (56144b6)

Open in CodSpeed

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.

Support pause HMR

1 participant