@samitouri / QOS-React / commits / f598ec1c29

[react-devtools-cdt-mcp] Add explicit register entry (#36972)

We should provide a way for consumers to handle the installation on their own, so the index entrypoint will just export the APIs and won't have any side-effects. The `/register` entrypoint (via `import react-devtools-cdt-mcp/register`), will subscribe an event listener and install the tools.

Ruslan Lesiutin committed Jul 16, 2026 at 18:15 UTC f598ec1c29c98c368e0a576d4e0cf58831038140
8 files changed +148 -66
packages/react-devtools-cdt-mcp/README.md
+15 -7
@@ -3,21 +3,29 @@
3 Integrates React tools with
4 [chrome-devtools-mcp](https://github.com/ChromeDevTools/chrome-devtools-mcp).
5
6 -Importing this package **before React** installs the DevTools hook and registers
7 -a React tool group via chrome-devtools-mcp's `devtoolstooldiscovery` / `__dtmcp`
8 -third-party-tool protocol. The React tools then become discoverable and callable
9 -inside a chrome-devtools-mcp session — no separate server.
6 +Importing `react-devtools-cdt-mcp/register` **before React** installs the
7 +DevTools hook and registers a React tool group via chrome-devtools-mcp's
8 +`devtoolstooldiscovery` / `__dtmcp` third-party-tool protocol. The React tools
9 +then become discoverable and callable inside a chrome-devtools-mcp session — no
10 +separate server.
11
12 ## Usage
13
13 -Import the package **before** React so the hook is installed before React
14 -initializes:
14 +Import the register entry **before** React so the hook is installed before
15 +React initializes:
16
17 ```js
17 -import 'react-devtools-cdt-mcp';
18 +import 'react-devtools-cdt-mcp/register';
19 import React from 'react';
20 ```
21
22 +The package root is side-effect-free and exports the lower-level API for custom
23 +targets:
24 +
25 +```js
26 +import {register, buildToolGroup} from 'react-devtools-cdt-mcp';
27 +```
28 +
29 When the page runs under chrome-devtools-mcp, the React tools are listed by
30 `list_3p_developer_tools` and callable either via
31 `execute_3p_developer_tool({toolName, params})` or directly via `evaluate_script`
packages/react-devtools-cdt-mcp/fixtures/app/index.js
+4 -4
@@ -1,7 +1,7 @@
1 -// Import the package source first so it installs the DevTools hook and registers
2 -// the chrome-devtools-mcp tool group BEFORE react-dom evaluates — the hook must
3 -// be in place when React's renderer injects on the first commit.
4 -import '../../src/index.js';
1 +// Import the register entry first so it installs the DevTools hook and
2 +// registers the chrome-devtools-mcp tool group BEFORE react-dom evaluates — the
3 +// hook must be in place when React's renderer injects on the first commit.
4 +import '../../src/register.js';
5
6 import * as React from 'react';
7 import {createRoot} from 'react-dom/client';
packages/react-devtools-cdt-mcp/package.json
+2 -1
@@ -11,7 +11,8 @@
11 },
12 "files": [
13 "dist",
14 - "index.js"
14 + "index.js",
15 + "register.js"
16 ],
17 "scripts": {
18 "build": "cross-env NODE_ENV=production rollup -c rollup.config.cjs",
packages/react-devtools-cdt-mcp/register.js new
+3
@@ -0,0 +1,3 @@
1 +'use strict';
2 +
3 +module.exports = require('./dist/register.js');
packages/react-devtools-cdt-mcp/rollup.config.cjs
+41 -32
@@ -9,35 +9,44 @@ if (!NODE_ENV) {
9 process.exit(1);
10 }
11
12 -module.exports = {
13 - input: 'src/index.js',
14 - // CommonJS bundle for the npm package (referenced by the root index.js stub).
15 - output: {
16 - file: 'dist/index.js',
17 - format: 'cjs',
18 - exports: 'named',
19 - },
20 - treeshake: {moduleSideEffects: false},
21 - plugins: [
22 - nodeResolve({
23 - extensions: ['.js', '.mjs'],
24 - }),
25 - commonjs({
26 - include: /node_modules/,
27 - }),
28 - babel({
29 - configFile: __dirname + '/../react-devtools-shared/babel.config.js',
30 - babelHelpers: 'bundled',
31 - }),
32 - replace({
33 - preventAssignment: true,
34 - values: {
35 - __DEV__: String(NODE_ENV === 'development'),
36 - __IS_CHROME__: 'false',
37 - __IS_FIREFOX__: 'false',
38 - __IS_EDGE__: 'false',
39 - __IS_NATIVE__: 'false',
40 - },
41 - }),
42 - ],
43 -};
12 +const plugins = [
13 + nodeResolve({
14 + extensions: ['.js', '.mjs'],
15 + }),
16 + commonjs({
17 + include: /node_modules/,
18 + }),
19 + babel({
20 + configFile: __dirname + '/../react-devtools-shared/babel.config.js',
21 + babelHelpers: 'bundled',
22 + }),
23 + replace({
24 + preventAssignment: true,
25 + values: {
26 + __DEV__: String(NODE_ENV === 'development'),
27 + __IS_CHROME__: 'false',
28 + __IS_FIREFOX__: 'false',
29 + __IS_EDGE__: 'false',
30 + __IS_NATIVE__: 'false',
31 + },
32 + }),
33 +];
34 +
35 +function createBundle(input, file) {
36 + return {
37 + input,
38 + // CommonJS bundles for the npm package (referenced by root stubs).
39 + output: {
40 + file,
41 + format: 'cjs',
42 + exports: 'named',
43 + },
44 + treeshake: {moduleSideEffects: false},
45 + plugins,
46 + };
47 +}
48 +
49 +module.exports = [
50 + createBundle('src/index.js', 'dist/index.js'),
51 + createBundle('src/register.js', 'dist/register.js'),
52 +];
packages/react-devtools-cdt-mcp/src/__tests__/DevToolsCdtMcp-test.js
+54 -3
@@ -94,7 +94,19 @@ describe('react-devtools-cdt-mcp', () => {
94 expect(globalThis.__dtmcp).toBeUndefined();
95 });
96
97 - it('throws when the auto entry is imported outside an event target', () => {
97 + it('root entry exports tools without registering', () => {
98 + unregister();
99 + delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
100 + jest.resetModules();
101 +
102 + const api = require('../index');
103 +
104 + expect(typeof api.register).toBe('function');
105 + expect(typeof api.buildToolGroup).toBe('function');
106 + expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeUndefined();
107 + });
108 +
109 + it('throws when the register entry is imported outside an event target', () => {
110 const originalAddEventListener = globalThis.addEventListener;
111 const originalRemoveEventListener = globalThis.removeEventListener;
112
@@ -108,8 +120,8 @@ describe('react-devtools-cdt-mcp', () => {
120 // $FlowFixMe[cannot-write]
121 globalThis.removeEventListener = undefined;
122
111 - expect(() => require('../index')).toThrow(
112 - 'react-devtools-cdt-mcp must be imported in a browser-like environment',
123 + expect(() => require('../register')).toThrow(
124 + 'react-devtools-cdt-mcp/register must be imported in a browser-like environment',
125 );
126 expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeUndefined();
127 } finally {
@@ -118,6 +130,45 @@ describe('react-devtools-cdt-mcp', () => {
130 }
131 });
132
133 + it('register entry installs the DevTools hook', () => {
134 + const originalAddEventListener = globalThis.addEventListener;
135 + const originalRemoveEventListener = globalThis.removeEventListener;
136 + let autoListener = null;
137 +
138 + unregister();
139 + delete globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
140 + jest.resetModules();
141 +
142 + try {
143 + // $FlowFixMe[cannot-write]
144 + globalThis.addEventListener = (type, listener, options) => {
145 + if (type === 'devtoolstooldiscovery') {
146 + autoListener = listener;
147 + }
148 + return originalAddEventListener.call(
149 + globalThis,
150 + type,
151 + listener,
152 + options,
153 + );
154 + };
155 +
156 + require('../register');
157 +
158 + expect(globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__).toBeDefined();
159 + } finally {
160 + if (autoListener !== null) {
161 + originalRemoveEventListener.call(
162 + globalThis,
163 + 'devtoolstooldiscovery',
164 + autoListener,
165 + );
166 + }
167 + globalThis.addEventListener = originalAddEventListener;
168 + globalThis.removeEventListener = originalRemoveEventListener;
169 + }
170 + });
171 +
172 it('returns the cached registration for repeated calls per target', () => {
173 let listener = null;
174 const target = {
packages/react-devtools-cdt-mcp/src/index.js
-19
@@ -7,23 +7,4 @@
7 * @flow
8 */
9
10 -import {register} from './DevToolsCdtMcp';
11 -
12 -// Side effect: install the facade (before React) and register the React tool
13 -// group for chrome-devtools-mcp. Import this module before React.
14 -if (
15 - typeof window !== 'undefined' &&
16 - typeof window.addEventListener === 'function' &&
17 - typeof window.removeEventListener === 'function'
18 -) {
19 - register();
20 -} else {
21 - // eslint-disable-next-line react-internal/prod-error-codes
22 - throw new Error(
23 - 'react-devtools-cdt-mcp must be imported in a browser-like environment ' +
24 - 'before React initializes. Use a client-only entry point, or the manual ' +
25 - 'entry point for custom targets.',
26 - );
27 -}
28 -
10 export * from './DevToolsCdtMcp';
packages/react-devtools-cdt-mcp/src/register.js new
+29
@@ -0,0 +1,29 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {register} from './DevToolsCdtMcp';
11 +
12 +// Side effect: install the facade (before React) and register the React tool
13 +// group for chrome-devtools-mcp. Import this module before React.
14 +if (
15 + typeof window !== 'undefined' &&
16 + typeof window.addEventListener === 'function' &&
17 + typeof window.removeEventListener === 'function'
18 +) {
19 + register();
20 +} else {
21 + // eslint-disable-next-line react-internal/prod-error-codes
22 + throw new Error(
23 + 'react-devtools-cdt-mcp/register must be imported in a browser-like ' +
24 + 'environment before React initializes. Use the root entry point for ' +
25 + 'side-effect-free access to register().',
26 + );
27 +}
28 +
29 +export * from './DevToolsCdtMcp';