feat(dev-hot-reload): streamline hot-reload process with ProcessManag…#7731
feat(dev-hot-reload): streamline hot-reload process with ProcessManag…#7731sid-bruno wants to merge 1 commit intousebruno:mainfrom
Conversation
…er and package discovery
WalkthroughThe dev launcher script transitions from a hardcoded Changes
Sequence Diagram(s)sequenceDiagram
participant Main as Dev Launcher
participant PM as ProcessManager
participant Discover as Package Discovery
participant Graph as Dependency Graph
participant Sort as Topological Sort
participant Builder as Build/Watch Executor
participant Watch as npm watch processes
participant Build as npm build processes
participant App as React/Electron Startup
Main->>Discover: Scan packages/*/package.json
Discover-->>Main: List `@usebruno/`* packages with build scripts
Main->>Graph: Build dependency graph from internal deps
Graph-->>Main: Dependency relations
Main->>Sort: Perform topological sort
Sort-->>Main: Build stages (or throw on cycle)
Main->>PM: Initialize ProcessManager
PM->>PM: Register signal handlers (SIGINT/SIGTERM/SIGHUP)
loop For each stage
Main->>Builder: Execute stage packages
par Parallel Processing
Builder->>Watch: npm run watch (if watch script exists)
Watch->>PM: Register child process
Watch-->>Builder: Wait for "created" substring output
Builder->>Build: npm run build (if no watch script)
Build->>PM: Register child process
Build-->>Builder: Resolve on completion or reject on exit code
end
Builder-->>Main: Stage complete
end
Main->>App: All watcher stages ready → Start React/Electron
App-->>Main: Application running
alt Process terminates or signal received
PM->>PM: Trigger coordinated shutdown
PM->>Watch: Kill child processes
PM->>Build: Kill child processes
PM->>PM: Force exit with timeout
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
scripts/dev-hot-reload.js (2)
132-139:⚠️ Potential issue | 🟠 Major
command -vis Unix-only; usewhereon Windows.Per the coding guidelines, shell commands must account for platform differences. This function will always return
falseon Windows.🛠️ Proposed 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', shell: true }); 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 commandExists function currently uses the Unix-only `command -v` which fails on Windows; update commandExists to choose the correct platform-specific probe (use `where <cmd>` when process.platform === 'win32' and `command -v <cmd>` otherwise), run the chosen probe via execSync (preserving { stdio: 'ignore' } and try/catch behavior), and return true on success or false on failure; reference the function name commandExists and the use of execSync in your changes.
161-165:⚠️ Potential issue | 🟠 MajorUnix-only shell commands break on Windows.
The
findandrm -rfcommands are not available on Windows. Consider using Node.js APIs or a cross-platform approach.🛠️ Proposed cross-platform fix using Node.js
+const { rmSync } = require('fs'); +const { globSync } = require('glob'); // or use fast-glob 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 dirs = globSync('**/node_modules', { onlyDirectories: true }); + for (const dir of dirs) { + rmSync(dir, { recursive: true, force: true }); + } log(LOG_LEVELS.SUCCESS, 'Node modules cleanup completed'); }Alternatively, if
globisn't available, usenpx rimraf '**/node_modules'which is cross-platform.🤖 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 uses Unix-only shell commands via execSync('find ... rm -rf ...'), which fails on Windows; replace that call with a cross-platform implementation: either use a Node.js API-based approach (e.g., use glob or fast-glob to locate '**/node_modules' and fs.rm or fs.rmdir with recursive=true to delete each directory) or run a cross-platform tool such as invoking 'npx rimraf "**/node_modules"' via execSync; update the execSync invocation in cleanNodeModules to the chosen approach and ensure stdio handling and error propagation remain consistent with the existing log(LOG_LEVELS.*) calls.
🧹 Nitpick comments (2)
scripts/dev-hot-reload.js (2)
204-205: Dependency graph excludespeerDependencies.Unlike
scripts/changed-packages.jswhich includesdependencies,devDependencies, andpeerDependencieswhen building its internal dependency map, this only considers the first two. If any@usebruno/*package declares another as a peerDependency, the build order may be incorrect.🔧 Suggested alignment with changed-packages.js
- const allDeps = { ...raw.dependencies, ...raw.devDependencies }; + const allDeps = { ...raw.dependencies, ...raw.devDependencies, ...raw.peerDependencies };🤖 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 204 - 205, The dependency collection in dev-hot-reload.js currently builds allDeps from raw.dependencies and raw.devDependencies only, causing peerDependencies to be ignored; update the logic that constructs allDeps (used by the variable internalDeps and functions that rely on it) to also include raw.peerDependencies so that Object.keys(allDeps).filter(k => k.startsWith('@usebruno/')) correctly picks up peer deps; ensure you handle the case where raw.peerDependencies may be undefined (merge safely) so internalDeps reflects dependencies, devDependencies, and peerDependencies consistently with changed-packages.js.
428-428:startDevelopment()is not awaited.Any rejection from
startDevelopment()after the main IIFE resolves won't be caught by the.catch()handler. Consider awaiting it:- 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` at line 428, The call to startDevelopment() is not awaited so its rejection can escape the enclosing IIFE's .catch; change the invocation so the async IIFE awaits or returns startDevelopment() (i.e., use await startDevelopment() or return startDevelopment()) so any thrown/rejected error from startDevelopment() is propagated into the existing .catch handler and handled consistently.
🤖 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 signal registration loop in scripts/dev-hot-reload.js
currently adds 'SIGHUP' (for const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) which
is unsupported on Windows; update the registration to skip or conditionally add
'SIGHUP' when process.platform !== 'win32' (or construct the signal list based
on platform) and keep the same handler this.shutdown(sig) for the remaining
signals so Windows won’t receive an unsupported signal registration.
- Around line 332-343: The promise waiting for a watcher ready signal (the new
Promise that registers onData on proc.stdout/proc.stderr and resolves when
data.includes('created ')) can hang if the output never contains "created "; add
a timeout guard and optional configurable ready signal: when creating the
promise in scripts/dev-hot-reload.js, accept/configure a timeoutMs and/or
readyPattern (env var or parameter), start a timer that will reject or resolve
(choose behavior) after timeoutMs, and ensure both the timer and the
stdout/stderr listeners (onData) are cleaned up in all code paths; update
references to proc, onData, log and LOG_LEVELS.SUCCESS/shortName so the handler
clears listeners and timer before resolving on match or finishing on timeout.
---
Outside diff comments:
In `@scripts/dev-hot-reload.js`:
- Around line 132-139: The commandExists function currently uses the Unix-only
`command -v` which fails on Windows; update commandExists to choose the correct
platform-specific probe (use `where <cmd>` when process.platform === 'win32' and
`command -v <cmd>` otherwise), run the chosen probe via execSync (preserving {
stdio: 'ignore' } and try/catch behavior), and return true on success or false
on failure; reference the function name commandExists and the use of execSync in
your changes.
- Around line 161-165: The cleanNodeModules function currently uses Unix-only
shell commands via execSync('find ... rm -rf ...'), which fails on Windows;
replace that call with a cross-platform implementation: either use a Node.js
API-based approach (e.g., use glob or fast-glob to locate '**/node_modules' and
fs.rm or fs.rmdir with recursive=true to delete each directory) or run a
cross-platform tool such as invoking 'npx rimraf "**/node_modules"' via
execSync; update the execSync invocation in cleanNodeModules to the chosen
approach and ensure stdio handling and error propagation remain consistent with
the existing log(LOG_LEVELS.*) calls.
---
Nitpick comments:
In `@scripts/dev-hot-reload.js`:
- Around line 204-205: The dependency collection in dev-hot-reload.js currently
builds allDeps from raw.dependencies and raw.devDependencies only, causing
peerDependencies to be ignored; update the logic that constructs allDeps (used
by the variable internalDeps and functions that rely on it) to also include
raw.peerDependencies so that Object.keys(allDeps).filter(k =>
k.startsWith('@usebruno/')) correctly picks up peer deps; ensure you handle the
case where raw.peerDependencies may be undefined (merge safely) so internalDeps
reflects dependencies, devDependencies, and peerDependencies consistently with
changed-packages.js.
- Line 428: The call to startDevelopment() is not awaited so its rejection can
escape the enclosing IIFE's .catch; change the invocation so the async IIFE
awaits or returns startDevelopment() (i.e., use await startDevelopment() or
return startDevelopment()) so any thrown/rejected error from startDevelopment()
is propagated into the existing .catch handler and handled consistently.
🪄 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: f0d6c321-6c73-4f35-9d26-ce67d11c3ee3
📒 Files selected for processing (1)
scripts/dev-hot-reload.js
| for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { | ||
| process.on(sig, () => this.shutdown(sig)); | ||
| } |
There was a problem hiding this comment.
SIGHUP is not supported on Windows.
Per the coding guidelines, avoid Unix-only signals without Windows fallbacks. SIGHUP will fail silently or error on Windows. Consider conditionally registering it:
🛠️ Proposed fix
- for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
+ const signals = process.platform === 'win32'
+ ? ['SIGINT', 'SIGTERM']
+ : ['SIGINT', 'SIGTERM', 'SIGHUP'];
+ for (const sig of signals) {
process.on(sig, () => this.shutdown(sig));
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) { | |
| process.on(sig, () => this.shutdown(sig)); | |
| } | |
| const signals = process.platform === 'win32' | |
| ? ['SIGINT', 'SIGTERM'] | |
| : ['SIGINT', 'SIGTERM', '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 signal registration loop
in scripts/dev-hot-reload.js currently adds 'SIGHUP' (for const sig of
['SIGINT', 'SIGTERM', 'SIGHUP']) which is unsupported on Windows; update the
registration to skip or conditionally add 'SIGHUP' when process.platform !==
'win32' (or construct the signal list based on platform) and keep the same
handler this.shutdown(sig) for the remaining signals so Windows won’t receive an
unsupported signal registration.
| return new Promise(resolve => { | ||
| const onData = data => { | ||
| if (data.toString().includes('created ')) { | ||
| proc.stdout.off('data', onData); | ||
| proc.stderr.off('data', onData); | ||
| log(LOG_LEVELS.SUCCESS, `${shortName} — initial watch build done`); | ||
| resolve(); | ||
| } | ||
| }; | ||
| proc.stdout.on('data', onData); | ||
| proc.stderr.on('data', onData); | ||
| }); |
There was a problem hiding this comment.
Potential hang if watcher output never contains "created ".
This assumes all watchers are rollup-based and emit "created ". If a package uses a different bundler or fails silently, this promise never resolves. Consider adding a timeout or a configurable ready signal.
🔧 Suggested timeout guard
if (pkg.hasWatch) {
const proc = procManager.register(
spawnWithPrefix('npm', ['run', 'watch', workspaceFlag], shortName, nextWatcherColor()),
shortName
);
- return new Promise(resolve => {
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(new Error(`${shortName} — timed out waiting for initial build`));
+ }, 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 waiting for a
watcher ready signal (the new Promise that registers onData on
proc.stdout/proc.stderr and resolves when data.includes('created ')) can hang if
the output never contains "created "; add a timeout guard and optional
configurable ready signal: when creating the promise in
scripts/dev-hot-reload.js, accept/configure a timeoutMs and/or readyPattern (env
var or parameter), start a timer that will reject or resolve (choose behavior)
after timeoutMs, and ensure both the timer and the stdout/stderr listeners
(onData) are cleaned up in all code paths; update references to proc, onData,
log and LOG_LEVELS.SUCCESS/shortName so the handler clears listeners and timer
before resolving on match or finishing on timeout.
There was a problem hiding this comment.
Pull request overview
This PR refactors the dev hot-reload script to remove the concurrently dependency and to orchestrate package watch/build startup via a custom ProcessManager, with a dependency-aware (topologically sorted) startup sequence before launching React and Electron.
Changes:
- Replace
concurrently-based orchestration with a customProcessManagerthat registers child processes and handles shutdown on signals / unexpected exits. - Auto-discover
@usebruno/*packages with abuildscript, topologically sort them by internal dependencies, and start watch/build stages in order. - Start React (
dev:web) and Electron (vianodemon) only after watchers report initial readiness.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| const tasks = stage.map(pkg => { | ||
| const shortName = pkg.name.replace('@usebruno/', ''); | ||
| const workspaceFlag = `--workspace=${pkg.dir}`; |
There was a problem hiding this comment.
workspaceFlag is built from pkg.dir (an absolute filesystem path). npm workspaces typically expect a workspace name (e.g. @usebruno/common) or a repo-relative path (e.g. packages/bruno-common). If npm doesn't recognize the absolute path, it can fall back to the root watch script (which exists) and recursively re-run this script. Use --workspace=${pkg.name} or --workspace=${path.relative(rootDir, pkg.dir)} instead.
| const workspaceFlag = `--workspace=${pkg.dir}`; | |
| const workspaceFlag = `--workspace=${pkg.name}`; |
| this._procs.push({ proc, name }); | ||
| proc.on('close', code => { | ||
| if (!this._shuttingDown && code !== null && code !== 0) { | ||
| log(LOG_LEVELS.ERROR, `Process "${name}" exited unexpectedly (code ${code}), shutting down...`); |
There was a problem hiding this comment.
ProcessManager.register doesn't attach a listener for the child process error event. If spawning fails (e.g. command missing, permissions), Node will emit error and it can become an unhandled 'error' event, crashing the script without a controlled shutdown. Add proc.on('error', ...) to log the failure and invoke shutdown() (and consider exiting non-zero).
| this._procs.push({ proc, name }); | |
| proc.on('close', code => { | |
| if (!this._shuttingDown && code !== null && code !== 0) { | |
| log(LOG_LEVELS.ERROR, `Process "${name}" exited unexpectedly (code ${code}), shutting down...`); | |
| this._procs.push({ proc, name }); | |
| proc.on('error', err => { | |
| if (!this._shuttingDown) { | |
| log(LOG_LEVELS.ERROR, `Process "${name}" failed to start or crashed with an error: ${err.message}`); | |
| process.exitCode = 1; | |
| this.shutdown('child-error'); | |
| } | |
| }); | |
| proc.on('close', code => { | |
| if (!this._shuttingDown && code !== null && code !== 0) { | |
| log(LOG_LEVELS.ERROR, `Process "${name}" exited unexpectedly (code ${code}), shutting down...`); | |
| process.exitCode = 1; |
| // Spawn a process and prefix every line of its output with [name] | ||
| function spawnWithPrefix(cmd, args, name, color) { | ||
| const prefix = `${color}[${name}]${COLORS.nc} `; | ||
| const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); | ||
|
|
||
| proc.stdout.on('data', data => | ||
| data.toString().split('\n').filter(Boolean).forEach(line => | ||
| process.stdout.write(prefix + line + '\n') | ||
| ) | ||
| ); | ||
| proc.stderr.on('data', data => | ||
| data.toString().split('\n').filter(Boolean).forEach(line => | ||
| process.stderr.write(prefix + line + '\n') | ||
| ) | ||
| ); |
There was a problem hiding this comment.
spawnWithPrefix splits each stdout/stderr chunk by \n and re-adds newlines. Stream chunk boundaries are arbitrary, so this can break lines (partial lines get treated as complete) and interleave prefixes incorrectly. Consider buffering per-stream until a newline is seen (or use readline on the stream) so prefixes are applied to complete lines only.
| // Spawn a process and prefix every line of its output with [name] | |
| function spawnWithPrefix(cmd, args, name, color) { | |
| const prefix = `${color}[${name}]${COLORS.nc} `; | |
| const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); | |
| proc.stdout.on('data', data => | |
| data.toString().split('\n').filter(Boolean).forEach(line => | |
| process.stdout.write(prefix + line + '\n') | |
| ) | |
| ); | |
| proc.stderr.on('data', data => | |
| data.toString().split('\n').filter(Boolean).forEach(line => | |
| process.stderr.write(prefix + line + '\n') | |
| ) | |
| ); | |
| function attachPrefixedOutput(stream, output, prefix) { | |
| let buffer = ''; | |
| stream.on('data', data => { | |
| buffer += data.toString(); | |
| let newlineIndex = buffer.indexOf('\n'); | |
| while (newlineIndex !== -1) { | |
| let line = buffer.slice(0, newlineIndex); | |
| if (line.endsWith('\r')) { | |
| line = line.slice(0, -1); | |
| } | |
| output.write(prefix + line + '\n'); | |
| buffer = buffer.slice(newlineIndex + 1); | |
| newlineIndex = buffer.indexOf('\n'); | |
| } | |
| }); | |
| stream.on('end', () => { | |
| if (buffer.length > 0) { | |
| output.write(prefix + buffer + '\n'); | |
| } | |
| }); | |
| } | |
| // Spawn a process and prefix every line of its output with [name] | |
| function spawnWithPrefix(cmd, args, name, color) { | |
| const prefix = `${color}[${name}]${COLORS.nc} `; | |
| const proc = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'] }); | |
| attachPrefixedOutput(proc.stdout, process.stdout, prefix); | |
| attachPrefixedOutput(proc.stderr, process.stderr, prefix); |
| shutdown(signal) { | ||
| if (this._shuttingDown) return; | ||
| this._shuttingDown = true; | ||
| log(LOG_LEVELS.INFO, `Received ${signal}, shutting down all processes...`); | ||
| for (const { proc } of this._procs) { | ||
| try { proc.kill(); } catch { /* already dead */ } | ||
| } | ||
| setTimeout(() => { | ||
| log(LOG_LEVELS.WARN, 'Force-exiting after timeout...'); | ||
| process.exit(0); | ||
| }, 5000).unref(); |
There was a problem hiding this comment.
shutdown() always force-exits with status 0 after the timeout, even when shutdown was triggered by an unexpected child exit. This makes it harder for callers/scripts to detect startup failures. Consider tracking a desired exit code (e.g. 1 on 'child-exit' / spawn errors) and using that in process.exit(...).
| return new Promise(resolve => { | ||
| const onData = data => { | ||
| if (data.toString().includes('created ')) { | ||
| proc.stdout.off('data', onData); | ||
| proc.stderr.off('data', onData); | ||
| log(LOG_LEVELS.SUCCESS, `${shortName} — initial watch build done`); | ||
| resolve(); | ||
| } | ||
| }; | ||
| proc.stdout.on('data', onData); | ||
| proc.stderr.on('data', onData); |
There was a problem hiding this comment.
The promise used to detect watcher readiness only resolves when output contains created . If the watch process exits early (e.g. rollup config error) before printing that line, this promise never resolves/rejects and startup hangs until the forced shutdown timeout. Consider rejecting on close (non-zero) and/or adding a readiness timeout with a clear error message.
| return new Promise(resolve => { | |
| const onData = data => { | |
| if (data.toString().includes('created ')) { | |
| proc.stdout.off('data', onData); | |
| proc.stderr.off('data', onData); | |
| log(LOG_LEVELS.SUCCESS, `${shortName} — initial watch build done`); | |
| resolve(); | |
| } | |
| }; | |
| proc.stdout.on('data', onData); | |
| proc.stderr.on('data', onData); | |
| return new Promise((resolve, reject) => { | |
| let isReady = false; | |
| const readinessTimeoutMs = 30000; | |
| const cleanup = () => { | |
| clearTimeout(readinessTimeout); | |
| proc.stdout.off('data', onData); | |
| proc.stderr.off('data', onData); | |
| proc.off('close', onClose); | |
| proc.off('error', onError); | |
| }; | |
| const onData = data => { | |
| if (data.toString().includes('created ')) { | |
| isReady = true; | |
| cleanup(); | |
| log(LOG_LEVELS.SUCCESS, `${shortName} — initial watch build done`); | |
| resolve(); | |
| } | |
| }; | |
| const onClose = code => { | |
| if (!isReady) { | |
| cleanup(); | |
| reject(new Error(`Watch failed for ${shortName} before readiness${code !== null ? ` (exit ${code})` : ''}`)); | |
| } | |
| }; | |
| const onError = err => { | |
| if (!isReady) { | |
| cleanup(); | |
| reject(err); | |
| } | |
| }; | |
| const readinessTimeout = setTimeout(() => { | |
| if (!isReady) { | |
| cleanup(); | |
| reject(new Error(`Timed out waiting for ${shortName} watch readiness after ${readinessTimeoutMs}ms`)); | |
| } | |
| }, readinessTimeoutMs); | |
| proc.stdout.on('data', onData); | |
| proc.stderr.on('data', onData); | |
| proc.on('close', onClose); | |
| proc.on('error', onError); |
| // Setup development environment | ||
| async function startDevelopment() { | ||
| const rootDir = path.join(__dirname, '..'); | ||
| const procManager = new ProcessManager(); | ||
|
|
||
| const stages = buildDevPackageGraph(rootDir); | ||
| for (let i = 0; i < stages.length; i++) { | ||
| await startWatcherStage(stages[i], procManager, i); | ||
| } |
There was a problem hiding this comment.
startDevelopment is declared async and can throw/reject (e.g. cycle detection in topoSort, failed one-time builds). However the current entrypoint doesn't await it, so failures can become unhandled promise rejections instead of being caught by the outer main().catch(...). Either await startDevelopment() in the entrypoint or make startDevelopment non-async and handle errors internally.
Description
Recreate #7599
Contribution Checklist:
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