Skip to content

feat(dev-hot-reload): streamline hot-reload #7599

Closed
sid-bruno wants to merge 1 commit intousebruno:mainfrom
sid-bruno:fix/hot-reload-streamline
Closed

feat(dev-hot-reload): streamline hot-reload #7599
sid-bruno wants to merge 1 commit intousebruno:mainfrom
sid-bruno:fix/hot-reload-streamline

Conversation

@sid-bruno
Copy link
Copy Markdown
Collaborator

@sid-bruno sid-bruno commented Mar 27, 2026

Description

Change to using just spawns and removing the dependency on concurrency and removes shell nesting issues for SIGINT

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • Chores
    • Improved the development build pipeline with enhanced process management and optimized startup sequencing for faster, more reliable hot-reload builds.

Copilot AI review requested due to automatic review settings March 27, 2026 13:52
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 27, 2026

Walkthrough

The dev hot-reload script replaces fixed concurrently-based orchestration with dynamic, dependency-aware pipeline management. It scans monorepo packages, topologically sorts internal dependencies, stages builds concurrently, waits for watch signals, and centrally manages process lifecycle through a new ProcessManager.

Changes

Cohort / File(s) Summary
Development Orchestration Pipeline
scripts/dev-hot-reload.js
Refactored from concurrently-based fixed command list to dynamic dependency-aware startup: added monorepo package scanning, topological sorting, ProcessManager for centralized signal/shutdown handling, spawn-based per-process logging, and watch-signal coordination between build stages. Removed ELECTRON_START_DELAY and global concurrently dependency.

Sequence Diagram(s)

sequenceDiagram
    actor Dev as Developer
    participant Script as dev-hot-reload.js
    participant PM as ProcessManager
    participant Build as Build Processes
    participant Watch as Watch Processes
    participant Electron as Electron/nodemon

    Dev->>Script: npm run dev
    Script->>Script: Scan packages & topo-sort
    
    par Stage 1 (Concurrent)
        Script->>Build: spawn npm run build (pkg A)
        Script->>Build: spawn npm run build (pkg B)
    end
    
    Build->>Build: Build completes
    
    par Stage 2 (Concurrent, Wait for Watch)
        Script->>Watch: spawn npm run watch (pkg A)
        Script->>Watch: spawn npm run watch (pkg B)
        Watch->>Watch: Emit "created " signal
        Watch-->>Script: Signal received
    end
    
    Script->>PM: Register watch processes
    PM->>PM: Listen for SIGINT/SIGTERM
    
    par Final Stage
        Script->>Electron: spawn nodemon with args
        Electron->>PM: Register process
    end
    
    Dev->>Dev: Code changes
    Watch->>Watch: Rebuild triggered
    Electron->>Electron: Hot reload via nodemon
    
    Dev->>Dev: Ctrl+C
    PM->>Build: Terminate all processes
    PM->>Watch: Terminate all processes
    PM->>Electron: Terminate electron
    PM->>Script: Exit cleanly
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • naman-bruno
  • bijin-bruno

Poem

🔄 A pipeline born from dependency's dance,
Stages orchestrated, no hard-coded chance,
Watch signals guide the concurrent parade,
Processes managed where ProcessManager played,
Dev mode now knows when to build, when to wait! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'streamline hot-reload' is vague and generic, using non-descriptive terms that don't convey the specific architectural changes (ProcessManager, dynamic dependency scanning, spawn-based orchestration) central to this high-effort refactoring. Consider a more specific title like 'feat(dev-hot-reload): replace concurrently with ProcessManager and dependency-aware spawn orchestration' to better reflect the substantial architectural changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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
Contributor

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
scripts/dev-hot-reload.js (3)

161-165: ⚠️ Potential issue | 🟠 Major

Unix-only shell command — fails on Windows.

find ... -exec rm -rf doesn't work on Windows. Use Node.js fs APIs or a cross-platform approach.

Cross-platform alternative using Node.js
+const { rmSync } = require('fs');
+
 function cleanNodeModules() {
   log(LOG_LEVELS.INFO, 'Removing all node_modules directories...');
-  execSync('find . -name "node_modules" -type d -prune -exec rm -rf {} +', { stdio: 'inherit' });
+  const removeNodeModules = (dir) => {
+    for (const entry of readdirSync(dir, { withFileTypes: true })) {
+      const fullPath = path.join(dir, entry.name);
+      if (entry.isDirectory()) {
+        if (entry.name === 'node_modules') {
+          log(LOG_LEVELS.DEBUG, `Removing ${fullPath}`);
+          rmSync(fullPath, { recursive: true, force: true });
+        } else if (entry.name !== '.git') {
+          removeNodeModules(fullPath);
+        }
+      }
+    }
+  };
+  removeNodeModules(path.join(__dirname, '..'));
   log(LOG_LEVELS.SUCCESS, 'Node modules cleanup completed');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-hot-reload.js` around lines 161 - 165, The cleanNodeModules
function currently runs a Unix-only shell command via execSync ('find ... -exec
rm -rf') which fails on Windows; replace that with a cross-platform
implementation: either use a reliable npm utility (e.g., rimraf) or use Node's
fs APIs (fs.promises.rm or fs.rmSync with recursive:true) combined with a glob
or directory-walk to locate "node_modules" folders. Update cleanNodeModules to
enumerate matching directories (starting from project root), remove them using
the chosen cross-platform approach, and keep the existing logging calls
(log(LOG_LEVELS.INFO/ SUCCESS, ...)); remove the execSync invocation and ensure
errors are handled/logged appropriately.

132-139: ⚠️ Potential issue | 🟠 Major

command -v is Unix-only — breaks on Windows.

This shell built-in doesn't exist on Windows. The function will always return false on Windows, triggering unnecessary global installs.

Cross-platform fix
 function commandExists(command) {
   try {
-    execSync(`command -v ${command}`, { stdio: 'ignore' });
+    const checkCmd = process.platform === 'win32'
+      ? `where ${command}`
+      : `command -v ${command}`;
+    execSync(checkCmd, { stdio: 'ignore' });
     return true;
   } catch {
     return false;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-hot-reload.js` around lines 132 - 139, The function commandExists
currently uses execSync(`command -v ${command}`) which is Unix-only and returns
false on Windows; update commandExists to be cross-platform by checking
process.platform and using the appropriate command (use 'where' on win32,
'command -v' or 'which' on posix) or by using a Node-native approach (e.g.,
child_process.spawnSync/execSync with platform-aware command or a small lookup
using require('child_process').spawnSync) to reliably detect the executable;
adjust references to execSync in commandExists so Windows correctly reports
installed commands and avoid forcing global installs.

427-428: ⚠️ Potential issue | 🟡 Minor

Missing await — errors from startDevelopment() won't be caught.

startDevelopment() is async but not awaited, so any rejection becomes unhandled.

   // Start development environment
-  startDevelopment();
+  await startDevelopment();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-hot-reload.js` around lines 427 - 428, The call to
startDevelopment() is not awaited so rejections become unhandled; change the
invocation to await startDevelopment() from an async context (or append
.catch(...)) and ensure errors are handled—either wrap the call in a try/catch
inside an async function (e.g., an immediately-invoked async wrapper) or call
startDevelopment().catch(err => processLogger.error(...)/process.exit(1));
update the invocation site where startDevelopment() is currently called so
Promise rejections are properly awaited and logged.
🧹 Nitpick comments (1)
scripts/dev-hot-reload.js (1)

332-343: Consider adding a timeout for the "created " signal.

If a watcher process fails to emit the expected signal, the promise hangs indefinitely. For a dev script this may be acceptable, but a timeout with a warning would improve debuggability.

Optional timeout implementation
       return new Promise(resolve => {
+        const timeout = setTimeout(() => {
+          log(LOG_LEVELS.WARN, `${shortName} — still waiting for initial build after 60s...`);
+        }, 60000);
         const onData = data => {
           if (data.toString().includes('created ')) {
             proc.stdout.off('data', onData);
             proc.stderr.off('data', onData);
+            clearTimeout(timeout);
             log(LOG_LEVELS.SUCCESS, `${shortName} — initial watch build done`);
             resolve();
           }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-hot-reload.js` around lines 332 - 343, The Promise that waits for
the "created " message can hang forever; add a timeout (e.g., 30s) that cleans
up listeners and logs a warning so the promise doesn't never resolve. Modify the
code around the new Promise where onData is defined: start a timer (store a
timeoutId) when attaching proc.stdout.on('data', onData) and
proc.stderr.on('data', onData), clear the timer inside onData on success (before
removing listeners and calling resolve), and in the timeout handler remove the
same listeners, log a warning (using log and LOG_LEVELS), and resolve or reject
the promise appropriately so callers won't hang; ensure you reference onData,
proc.stdout/proc.stderr, shortName and LOG_LEVELS.SUCCESS when implementing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/dev-hot-reload.js`:
- Around line 75-77: The loop registering signal handlers currently includes
'SIGHUP', which is unsupported on Windows; update the registration so 'SIGHUP'
is only added on non-Windows platforms (e.g., check process.platform !== 'win32'
or use os.platform()) and keep the existing handlers for 'SIGINT' and 'SIGTERM'
calling this.shutdown(sig) — locate the for-loop that iterates over
['SIGINT','SIGTERM','SIGHUP'] and change it to conditionally include 'SIGHUP'
(or register it separately inside an if block) to avoid a no-op listener on
Windows.

---

Outside diff comments:
In `@scripts/dev-hot-reload.js`:
- Around line 161-165: The cleanNodeModules function currently runs a Unix-only
shell command via execSync ('find ... -exec rm -rf') which fails on Windows;
replace that with a cross-platform implementation: either use a reliable npm
utility (e.g., rimraf) or use Node's fs APIs (fs.promises.rm or fs.rmSync with
recursive:true) combined with a glob or directory-walk to locate "node_modules"
folders. Update cleanNodeModules to enumerate matching directories (starting
from project root), remove them using the chosen cross-platform approach, and
keep the existing logging calls (log(LOG_LEVELS.INFO/ SUCCESS, ...)); remove the
execSync invocation and ensure errors are handled/logged appropriately.
- Around line 132-139: The function commandExists currently uses
execSync(`command -v ${command}`) which is Unix-only and returns false on
Windows; update commandExists to be cross-platform by checking process.platform
and using the appropriate command (use 'where' on win32, 'command -v' or 'which'
on posix) or by using a Node-native approach (e.g.,
child_process.spawnSync/execSync with platform-aware command or a small lookup
using require('child_process').spawnSync) to reliably detect the executable;
adjust references to execSync in commandExists so Windows correctly reports
installed commands and avoid forcing global installs.
- Around line 427-428: The call to startDevelopment() is not awaited so
rejections become unhandled; change the invocation to await startDevelopment()
from an async context (or append .catch(...)) and ensure errors are
handled—either wrap the call in a try/catch inside an async function (e.g., an
immediately-invoked async wrapper) or call startDevelopment().catch(err =>
processLogger.error(...)/process.exit(1)); update the invocation site where
startDevelopment() is currently called so Promise rejections are properly
awaited and logged.

---

Nitpick comments:
In `@scripts/dev-hot-reload.js`:
- Around line 332-343: The Promise that waits for the "created " message can
hang forever; add a timeout (e.g., 30s) that cleans up listeners and logs a
warning so the promise doesn't never resolve. Modify the code around the new
Promise where onData is defined: start a timer (store a timeoutId) when
attaching proc.stdout.on('data', onData) and proc.stderr.on('data', onData),
clear the timer inside onData on success (before removing listeners and calling
resolve), and in the timeout handler remove the same listeners, log a warning
(using log and LOG_LEVELS), and resolve or reject the promise appropriately so
callers won't hang; ensure you reference onData, proc.stdout/proc.stderr,
shortName and LOG_LEVELS.SUCCESS when implementing.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 828e9d21-cbb1-4aea-a460-f91e6553e603

📥 Commits

Reviewing files that changed from the base of the PR and between 95de14a and 51f9f50.

📒 Files selected for processing (1)
  • scripts/dev-hot-reload.js

Comment thread scripts/dev-hot-reload.js
Comment on lines +75 to +77
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
process.on(sig, () => this.shutdown(sig));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SIGHUP is Unix-only — ineffective on Windows.

Windows doesn't support SIGHUP. Node.js will ignore the listener or it simply won't fire. Consider conditionally registering it or removing it for a cleaner cross-platform experience.

Suggested fix
-    for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
+    const signals = ['SIGINT', 'SIGTERM'];
+    if (process.platform !== 'win32') {
+      signals.push('SIGHUP');
+    }
+    for (const sig of signals) {
       process.on(sig, () => this.shutdown(sig));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/dev-hot-reload.js` around lines 75 - 77, The loop registering signal
handlers currently includes 'SIGHUP', which is unsupported on Windows; update
the registration so 'SIGHUP' is only added on non-Windows platforms (e.g., check
process.platform !== 'win32' or use os.platform()) and keep the existing
handlers for 'SIGINT' and 'SIGTERM' calling this.shutdown(sig) — locate the
for-loop that iterates over ['SIGINT','SIGTERM','SIGHUP'] and change it to
conditionally include 'SIGHUP' (or register it separately inside an if block) to
avoid a no-op listener on Windows.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the dev-hot-reload script to remove the concurrently dependency and replace it with a spawn-based process manager intended to improve signal (SIGINT) propagation and streamline watcher startup.

Changes:

  • Replaces concurrently with a custom ProcessManager and spawn-based execution with prefixed logs.
  • Auto-discovers buildable @usebruno/* packages, topologically sorts them by internal dependencies, and starts watchers in stages.
  • Updates Electron restart logic to run nodemon without an extra shell wrapper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/dev-hot-reload.js
Comment on lines +383 to +392
// Build nodemon args without a shell wrapper so signals propagate correctly
const nodemonArgs = [
...CONFIG.ELECTRON_WATCH_PATHS.flatMap(p => ['--watch', p]),
'--ext', 'js,jsx,ts,tsx',
'--delay', `${CONFIG.NODEMON_WATCH_DELAY}ms`,
'--exec', 'npm run dev --workspace=packages/bruno-electron'
];
procManager.register(
spawnWithPrefix('nodemon', nodemonArgs, 'electron', '\x1b[33m'),
'electron'
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

The nodemon invocation passes --exec as a single argv element containing spaces ('npm run dev --workspace=packages/bruno-electron'). nodemon does not shell-parse/split this value; it will treat the entire string as the executable name, which will fail to spawn. Pass --exec as 'npm' and provide the remaining tokens (run, dev, --workspace=...) as subsequent arguments (or use nodemon’s -- args mechanism).

Copilot uses AI. Check for mistakes.
Comment thread scripts/dev-hot-reload.js
Comment on lines +98 to +101
setTimeout(() => {
log(LOG_LEVELS.WARN, 'Force-exiting after timeout...');
process.exit(0);
}, 5000).unref();
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

ProcessManager.shutdown() always process.exit(0) after the timeout, even when it was triggered by an unexpected child exit (register()’s close handler) or a failed build. This can report success despite failures. Consider tracking an exit code (e.g. set to 1 on child failure) and exiting with that code.

Copilot uses AI. Check for mistakes.
Comment thread scripts/dev-hot-reload.js
Comment on lines +345 to +348
// One-time build — not registered with ProcessManager
log(LOG_LEVELS.INFO, `${shortName} — running one-time build (no watch script)`);
return new Promise((resolve, reject) => {
const proc = spawn('npm', ['run', 'build', workspaceFlag], { stdio: 'inherit' });
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

One-time build processes (packages without a watch script) are explicitly not registered with ProcessManager. If the user hits SIGINT during these builds, shutdown() won’t kill them and they can continue running orphaned in the background. Consider registering these build procs as well (possibly with a flag to allow normal exit) so they’re included in shutdown cleanup.

Suggested change
// One-time build — not registered with ProcessManager
log(LOG_LEVELS.INFO, `${shortName} — running one-time build (no watch script)`);
return new Promise((resolve, reject) => {
const proc = spawn('npm', ['run', 'build', workspaceFlag], { stdio: 'inherit' });
// One-time build — register with ProcessManager so it is cleaned up on shutdown
log(LOG_LEVELS.INFO, `${shortName} — running one-time build (no watch script)`);
return new Promise((resolve, reject) => {
const proc = procManager.register(
spawn('npm', ['run', 'build', workspaceFlag], { stdio: 'inherit' }),
shortName
);

Copilot uses AI. Check for mistakes.
Comment thread scripts/dev-hot-reload.js
Comment on lines 421 to 422
// Run setup if requested
if (runSetup) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

startDevelopment is now async, but it’s invoked later without await. If buildDevPackageGraph throws (e.g. cycle) or a one-time build rejects, the rejection won’t be caught by the outer .catch, and the script may emit an unhandled promise rejection. Ensure the call site awaits/returns the startDevelopment() promise so failures terminate deterministically.

Copilot uses AI. Check for mistakes.
Comment thread scripts/dev-hot-reload.js
Comment on lines +324 to +329
const shortName = pkg.name.replace('@usebruno/', '');
const workspaceFlag = `--workspace=${pkg.dir}`;

if (pkg.hasWatch) {
const proc = procManager.register(
spawnWithPrefix('npm', ['run', 'watch', workspaceFlag], shortName, nextWatcherColor()),
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

workspaceFlag is built from pkg.dir, which is an absolute path (path.join(rootDir, 'packages', folder)). Everywhere else in the repo uses --workspace=packages/<name> (relative), and npm’s --workspace generally expects a workspace name or a path relative to the workspace root. Consider storing a relative workspace spec (e.g. packages/${folder}) or using the package name instead, to avoid npm failing to resolve the workspace.

Copilot uses AI. Check for mistakes.
@sid-bruno sid-bruno closed this by deleting the head repository Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants