Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions src/__tests__/cli-react-devtools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ function mockRemoteCompanionSuccess(): void {
statePath: '/tmp/state.json',
logPath: '/tmp/companion.log',
});
vi.mocked(stopReactDevtoolsCompanion).mockResolvedValueOnce({
stopped: true,
statePath: '/tmp/state.json',
});
}

function assertNoRemoteCompanion(): void {
Expand Down Expand Up @@ -125,17 +121,11 @@ function assertRemoteCompanionStarted(env: NodeJS.ProcessEnv): void {
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[0], 'npm');
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[2]?.cwd, '/tmp/project');
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[2]?.env, env);
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 1);
assert.deepEqual(vi.mocked(stopReactDevtoolsCompanion).mock.calls[0]?.[0], {
projectRoot: '/tmp/project',
stateDir: '/tmp/agent-device-state',
profileKey: '/tmp/remote.json',
consumerKey: 'default',
});
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 0);
}

for (const { label, leaseBackend } of remoteBridgeBackends) {
test(`react-devtools starts remote ${label} companion around passthrough command`, async () => {
test(`react-devtools keeps remote ${label} companion after passthrough command`, async () => {
const env = { ...process.env };
mockRemoteCompanionSuccess();

Expand All @@ -157,6 +147,52 @@ for (const { label, leaseBackend } of remoteBridgeBackends) {
});
}

test('react-devtools stop cleans up remote companion', async () => {
const env = { ...process.env };
vi.mocked(runCmdStreaming).mockResolvedValueOnce({
exitCode: 0,
stdout: '',
stderr: '',
});
vi.mocked(stopReactDevtoolsCompanion).mockResolvedValueOnce({
stopped: true,
statePath: '/tmp/state.json',
});

const exitCode = await runReactDevtoolsCommand(['stop'], {
stateDir: '/tmp/agent-device-state',
session: 'default',
cwd: '/tmp/project',
env,
flags: {
...remoteBridgeScope,
leaseBackend: 'ios-instance',
remoteConfig: '/tmp/remote.json',
session: 'default',
},
});

assert.equal(exitCode, 0);
assert.equal(vi.mocked(ensureReactDevtoolsCompanion).mock.calls.length, 0);
assert.equal(vi.mocked(runCmdStreaming).mock.calls[0]?.[0], 'npm');
assert.deepEqual(vi.mocked(runCmdStreaming).mock.calls[0]?.[1], [
'exec',
'--yes',
'--package',
'agent-react-devtools@0.4.0',
'--',
'agent-react-devtools',
'stop',
]);
assert.equal(vi.mocked(stopReactDevtoolsCompanion).mock.calls.length, 1);
assert.deepEqual(vi.mocked(stopReactDevtoolsCompanion).mock.calls[0]?.[0], {
projectRoot: '/tmp/project',
stateDir: '/tmp/agent-device-state',
profileKey: '/tmp/remote.json',
consumerKey: 'default',
});
});

test('react-devtools skips companion for non-bridge remote sessions', async () => {
await runStatusWithoutCompanion({
...remoteBridgeScope,
Expand Down
28 changes: 17 additions & 11 deletions src/cli/commands/react-devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function resolveRemoteBridgeConfig(
}

async function withRemoteDevtoolsCompanion<T>(
args: string[],
options: ReactDevtoolsCommandOptions,
action: () => Promise<T>,
): Promise<T> {
Expand All @@ -96,6 +97,20 @@ async function withRemoteDevtoolsCompanion<T>(
const session = options.session ?? flags?.session ?? 'default';
const profileKey =
flags?.remoteConfig ?? `${bridgeConfig.tenantId}:${bridgeConfig.runId}:${bridgeConfig.leaseId}`;

if (args[0] === 'stop') {
try {
return await action();
} finally {
await stopReactDevtoolsCompanion({
projectRoot: options.cwd ?? process.cwd(),
stateDir,
profileKey,
consumerKey: session,
});
}
}

await ensureReactDevtoolsCompanion({
projectRoot: options.cwd ?? process.cwd(),
stateDir,
Expand All @@ -111,16 +126,7 @@ async function withRemoteDevtoolsCompanion<T>(
consumerKey: session,
env: options.env ?? process.env,
});
try {
return await action();
} finally {
await stopReactDevtoolsCompanion({
projectRoot: options.cwd ?? process.cwd(),
stateDir,
profileKey,
consumerKey: session,
});
}
return await action();
}

export async function runReactDevtoolsCommand(
Expand All @@ -129,7 +135,7 @@ export async function runReactDevtoolsCommand(
): Promise<number> {
const cwd = options.cwd ?? process.cwd();
const env = options.env ?? process.env;
return await withRemoteDevtoolsCompanion(options, async () => {
return await withRemoteDevtoolsCompanion(args, options, async () => {
const result = await runCmdStreaming('npm', buildReactDevtoolsNpmExecArgs(args), {
cwd,
env,
Expand Down
4 changes: 2 additions & 2 deletions src/utils/command-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ Rules:
@c refs reset after reload/remount. After reload, wait --connected and inspect again.
Keep the profile window narrow; unrelated navigation makes render data noisy.
For cross-platform validation with explicit device selectors, prefer isolated --state-dir and restart react-devtools between platforms.
Remote bridge sessions (Android and iOS) run normally through agent-device react-devtools; the CLI manages the needed local service tunnel. Expo support depends on the SDK's bundled React Native runtime.
Remote Android and iOS bridge runs normally through agent-device react-devtools; the CLI keeps the needed local service tunnel alive until agent-device react-devtools stop or disconnect. Expo support depends on the SDK's bundled React Native runtime.

Example:
agent-device react-devtools status
Expand Down Expand Up @@ -496,7 +496,7 @@ Rules:
For self-contained scripts, pass the same --remote-config to every operational command, including disconnect; a preceding connect is optional but not required.
For remote artifact installs, use install-from-source <url> or install-from-source --github-actions-artifact org/repo:artifact; do not download CI artifacts locally first.
After connect, let the active remote connection supply runtime hints.
For remote Android and iOS bridge React DevTools, run agent-device react-devtools normally. The CLI opens the needed local service tunnel for the DevTools daemon and cleans it up when the command exits.
For remote Android and iOS bridge React DevTools, run agent-device react-devtools normally. The CLI opens the needed local service tunnel for the DevTools daemon and keeps it alive until agent-device react-devtools stop or disconnect.
Use --debug when remote connection or transport errors need diagnostic ids and remote log hints.`,
},
macos: {
Expand Down
2 changes: 1 addition & 1 deletion website/docs/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -590,7 +590,7 @@ agent-device react-devtools profile rerenders --limit 5
- Use it when a React Native workflow needs component hierarchy, props, state, hooks, render causes, slow components, or re-render counts.
- Keep using `snapshot`, `press`, `fill`, `logs`, `network`, and `perf` for device/app runtime evidence. Use `react-devtools` for React internals.
- React Native development builds can connect to the DevTools daemon on port 8097. For Android emulators or physical devices, run `adb reverse tcp:8097 tcp:8097` if the app cannot reach the host. If Metro is local, also run `adb reverse tcp:8081 tcp:8081`.
- For Android and iOS sessions connected through a remote bridge profile, `react-devtools` registers a lease-scoped companion tunnel to the sandbox-local DevTools daemon at `127.0.0.1:8097`. Android bridge profiles use the bridge-owned remote `adb reverse` mapping; iOS bridge profiles use the bridge-owned wildcard Metro host tunnel. The CLI unregisters the companion when the command exits.
- For Android and iOS sessions connected through a remote bridge profile, `react-devtools` registers a lease-scoped companion tunnel to the sandbox-local DevTools daemon at `127.0.0.1:8097`. Android bridge profiles use the bridge-owned remote `adb reverse` mapping; iOS bridge profiles use the bridge-owned wildcard Metro host tunnel. The CLI keeps the companion alive until `agent-device react-devtools stop` or `agent-device disconnect`.
- Remote bridge React DevTools assumes the React Native-bundled DevTools behavior in React Native 0.83+. Older browser/Chromium DevTools workflows are not assumed to exist inside remote sandboxes. Expo projects should be verified against the SDK's bundled React Native version before relying on this path; this release does not claim a separately verified Expo SDK version.
- For cross-platform validation with explicit target selectors, prefer an isolated `--state-dir` over separate named sessions. Named sessions enable bound-session locks during setup. Restart `react-devtools` between iOS and Android runs.

Expand Down
Loading