Skip to content

Commit 370ef23

Browse files
authored
fix: use static import in connect module to prevent race condition (#15)
* fix: prevent race condition in connect module with static import The connect module used a dynamic `await import('react-devtools-core')` which created two problems: 1. The async import yielded control before `initialize()` could install the devtools hook, allowing react-dom to load first and miss it. 2. Vite's esbuild dep optimizer splits dynamic imports into separate CJS chunks that only expose a `default` export, so named destructuring of `{ initialize, connectToDevTools }` silently returned undefined. Switch to a static import so initialize() runs synchronously at module evaluation time, before any dependent modules execute. The Vite plugin's config hook enables top-level-await support in esbuild (kept for future use), though this fix no longer requires it. react-devtools-core is a required peer dependency — if not installed, the module correctly fails to load since it cannot function without it. Fixes #14 * docs: add headed browser requirement for agent-browser usage Headless Chromium does not properly execute ES module scripts, preventing the devtools connect script from installing the hook before React loads. Document that agent-browser must use --headed mode in the SKILL.md, setup guide, and README.
1 parent 3853c8b commit 370ef23

File tree

6 files changed

+82
-13
lines changed

6 files changed

+82
-13
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"agent-react-devtools": patch
3+
---
4+
5+
fix: prevent race condition where React loads before devtools hook is installed
6+
7+
The connect module's dynamic `import('react-devtools-core')` yielded control before `initialize()` could install the hook, allowing react-dom to load first and miss the connection. Added top-level `await` to block dependent modules until the hook is ready, and updated the Vite plugin to enable `top-level-await` in esbuild's dep optimizer.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,15 @@ For Expo, the connection works automatically with the Expo dev client.
198198

199199
To use a custom port, set the `REACT_DEVTOOLS_PORT` environment variable.
200200

201+
## Using with agent-browser
202+
203+
When using `agent-browser` to drive the app (e.g. for profiling interactions), you **must use headed mode**. Headless Chromium does not properly execute the devtools connect script:
204+
205+
```sh
206+
agent-browser --session devtools --headed open http://localhost:5173/
207+
agent-react-devtools status # Should show "Apps: 1 connected"
208+
```
209+
201210
## Using with AI Coding Assistants
202211

203212
Add the skill to your AI coding assistant for richer context:

packages/agent-react-devtools/skills/react-devtools/SKILL.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,20 @@ agent-react-devtools profile slow --limit 5
124124
# Compare render counts and durations to the previous run
125125
```
126126

127+
## Using with agent-browser
128+
129+
When using `agent-browser` to drive the app while profiling or debugging, you **must use headed mode** (`--headed`). Headless Chromium does not execute ES module scripts the same way as a real browser, which prevents the devtools connect script from running properly.
130+
131+
```bash
132+
agent-browser --session devtools --headed open http://localhost:5173/
133+
agent-react-devtools status # Should show 1 connected app
134+
```
135+
127136
## Important Rules
128137

129138
- **Labels reset** when the app reloads or components unmount/remount. Always re-check with `get tree` or `find` after a page reload.
130139
- **`status` first** — if status shows 0 connected apps, the React app is not connected. The user may need to run `npx agent-react-devtools init` in their project first.
140+
- **Headed browser required** — if using `agent-browser`, always use `--headed` mode. Headless Chromium does not properly load the devtools connect script.
131141
- **Profile while interacting** — profiling only captures renders that happen between `profile start` and `profile stop`. Make sure the relevant interaction happens during that window.
132142
- **Use `--depth`** on large trees — a deep tree can produce a lot of output. Start with `--depth 3` or `--depth 4` and go deeper only on the subtree you care about.
133143

packages/agent-react-devtools/skills/react-devtools/references/setup.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,16 @@ If `Apps: 0 connected`:
9898
1. Check the app is running in dev mode
9999
2. Check the console for WebSocket connection errors
100100
3. Ensure no other DevTools instance is using port 8097
101+
4. If using `agent-browser`, make sure you're using **headed mode** (`--headed`) — headless Chromium does not properly execute the devtools connect script
102+
103+
## Using with agent-browser
104+
105+
When automating the browser with `agent-browser`, you must use headed mode. Headless Chromium handles ES module script execution differently, which prevents the connect script from installing the devtools hook before React loads.
106+
107+
```bash
108+
# Headed mode is required for devtools to connect
109+
agent-browser --session devtools --headed open http://localhost:5173/
110+
111+
# Verify connection
112+
agent-react-devtools status
113+
```

packages/agent-react-devtools/src/connect.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@
55
*
66
* This must be imported before React loads. It:
77
* 1. Removes the Vite plugin-react hook stub
8-
* 2. Initializes react-devtools-core
8+
* 2. Initializes react-devtools-core (installs the real __REACT_DEVTOOLS_GLOBAL_HOOK__)
99
* 3. Connects via WebSocket to the agent-react-devtools daemon
1010
*
11+
* Steps 1–2 run synchronously at module evaluation time via a static import
12+
* of react-devtools-core. This is critical — a dynamic import would yield
13+
* control and let React load before the hook is installed. The static import
14+
* also ensures esbuild's CJS-to-ESM interop provides proper named exports
15+
* (dynamic imports to CJS chunks only expose a default export).
16+
*
17+
* react-devtools-core is a required peer dependency. If not installed, this
18+
* module will fail to load — which is the correct behavior since there's
19+
* nothing useful it can do without it.
20+
*
1121
* Export `ready` — a promise that resolves once the WebSocket opens
1222
* (or after a 2s timeout / error, so the app is never blocked).
1323
*/
1424

25+
import { initialize, connectToDevTools } from 'react-devtools-core';
26+
1527
function getMeta(name: string): string | null {
1628
if (typeof document === 'undefined') return null;
1729
const meta = document.querySelector(`meta[name="${name}"]`);
@@ -41,23 +53,27 @@ const isProd =
4153
(typeof process !== 'undefined' &&
4254
process.env?.NODE_ENV === 'production');
4355

56+
// Install the devtools hook synchronously before React loads.
57+
// This MUST happen at module evaluation time — if deferred to an async
58+
// callback, react-dom may initialize first and miss the hook entirely.
59+
if (!isSSR && !isProd) {
60+
// Remove Vite's plugin-react hook stub so react-devtools-core can install the full hook
61+
try {
62+
delete (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
63+
} catch {
64+
// Property may be non-configurable (browser extension) — ignore
65+
}
66+
67+
initialize();
68+
}
69+
4470
export const ready: Promise<void> = isSSR || isProd ? noop() : connect();
4571

46-
async function connect(): Promise<void> {
72+
function connect(): Promise<void> {
4773
try {
4874
const port = getPort();
4975
const host = getHost();
5076

51-
// Remove Vite's plugin-react hook stub so react-devtools-core can install the full hook
52-
try {
53-
delete (window as any).__REACT_DEVTOOLS_GLOBAL_HOOK__;
54-
} catch {
55-
// Property may be non-configurable (browser extension) — ignore
56-
}
57-
58-
const { initialize, connectToDevTools } = await import('react-devtools-core');
59-
initialize();
60-
6177
return new Promise<void>((resolve) => {
6278
try {
6379
const ws = new WebSocket(`ws://${host}:${port}`);
@@ -70,6 +86,6 @@ async function connect(): Promise<void> {
7086
}
7187
});
7288
} catch {
73-
// react-devtools-core not installed or other error — silently skip
89+
return Promise.resolve();
7490
}
7591
}

packages/agent-react-devtools/src/vite-plugin.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ export function reactDevtools(options?: ReactDevtoolsOptions): Plugin {
1414
return {
1515
name: 'agent-react-devtools',
1616
apply: 'serve',
17+
config() {
18+
// The connect module uses top-level await to block React from loading
19+
// before the devtools hook is installed. Vite's dep optimizer uses
20+
// esbuild which defaults to es2020 (no TLA support), so we enable it.
21+
return {
22+
optimizeDeps: {
23+
esbuildOptions: {
24+
supported: {
25+
'top-level-await': true,
26+
},
27+
},
28+
},
29+
};
30+
},
1731
transformIndexHtml: {
1832
order: 'pre',
1933
handler() {

0 commit comments

Comments
 (0)