Skip to content

feat(middleware): discover debug targets through one Rozenite endpoint - #490

Merged
V3RON merged 6 commits into
mainfrom
feat/unified-target-discovery
Sep 3, 2026
Merged

V3RON merged 6 commits into
mainfrom
feat/unified-target-discovery

Conversation

@V3RON

@V3RON V3RON commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Description

Makes GET /rozenite/agent/targets the single way Rozenite code discovers debug targets, on both React Native (Metro) and Lynx, and records the decision as the repository's first ADR (docs/adr/0000-single-target-discovery-endpoint.md).

  • @rozenite/middleware stamps each target with integration (react-native | lynx), taken from its own config rather than guessed from a port.
  • rozenite open fetches the endpoint from each scanned dev server and labels the picker from integration. Its private copy of the /json/list selection rules is deleted.
  • @rozenite/app re-resolves a target after a recoverable disconnect through the same endpoint, filtering on deviceId and preferring the page it was debugging. Its private copy of the selection rules is deleted.
  • MetroTarget.pageId is now the page's id within its device (the page query parameter of webSocketDebuggerUrl); id keeps the globally unique composite.
  • The CLI now distinguishes "dev server not listening" from "dev server answered with an error" when it finds no targets.
  • Adds docs/adr/README.md and the ADR.

Related Issue

Closes #482

Context

The issue proposes extracting the pure selection rules into a shared module. That fixes the drift but not extensibility, since every consumer would still parse /json/list itself. The endpoint already existed for the Agent SDK and is already served on the Lynx dev server, because @rozenite/lynx-dev mounts the full middleware under /rozenite. Making it canonical removes two of the three copies and gives new target properties a single home. The ADR spells out the scenarios: the React Native DevTools injection path never discovers targets and is untouched, and @rozenite/web's handleOpenDebugger keeps its /json/list call on purpose because it mirrors the React Native CLI.

Host and port resolution for the middleware's internal /json/list call is deliberately unchanged. The CLI does not fall back to /json/list, since the CLI and middleware are released in lockstep.

Two adversarial review passes caught that the app compared the page query value against the composite <deviceId>-<pageId> id, so the "reconnect to the same page" rule never fired. Fixing pageId in the one mapper resolves it for every consumer. The second pass caught that both fetchers checked HTTP status before the body, discarding the endpoint's error message, since the middleware always pairs ok:false with a 400 or 404; both now parse the envelope first.

Testing

  • git fetch origin main && pnpm checks:affected && pnpm test:affected — all green (middleware, CLI, and app suites all green).
  • pnpm release:plan — changeset present, minor bump on the fixed @rozenite/* set.
  • New unit suite packages/middleware/src/__tests__/metro-discovery.test.ts runs the selection rules against a real local HTTP server, including integration stamping and pageId extraction.
  • Reconnect test in the app feeds the URL-derived page id and asserts it lands on the same page.
  • pnpm --filter @rozenite/app build inspected: no node: specifiers, express, or ws in the browser bundle.

Implements docs/adr/0000-single-target-discovery-endpoint.md: GET
/rozenite/agent/targets is now the only way Rozenite code discovers debug
targets. The middleware's mapper is the single implementation of the
selection rules and now stamps each target with which integration
(react-native or lynx) serves it; the CLI and the app no longer parse
/json/list themselves and instead fetch this endpoint and read that field
directly rather than guessing it from a scanned port.
- pageId now names a page within its own device (the `page` query
  parameter of webSocketDebuggerUrl), not the globally unique
  <deviceId>-<pageId> composite, so a reconnect's preferred-page match
  in @rozenite/app actually matches.
- Validate the /rozenite/agent/targets envelope shape in both the app's
  and the CLI's fetchers before trusting `ok`/`error`/`result.targets`,
  throwing the unreachable/unexpected-response error on a malformed
  body instead of letting a bare TypeError burn all reconnect attempts.
- Port the deleted CLI selection-rule tests to
  packages/middleware/src/__tests__/metro-discovery.test.ts against the
  real getMetroTargets/resolveMetroTarget, and add session-manager
  coverage for the lynx/lynx-web host integration mapping.
- Give DevServerFailure a `kind` ('unreachable' | 'error') so
  formatNoTargetsMessage only tells someone to start their dev server
  when every scanned port was actually unreachable, not when a server
  answered with its own error.
- Reword stale /json/list-era comments to point at the middleware's
  targets endpoint, and rewrite the garbled reconnect-preference
  comment in metro-target-resolution.ts.
- Derive hostIntegration with an exhaustive switch over
  RozeniteIntegration instead of a ternary that failed open to
  react-native.
- Fall back to the bare target label in the CLI picker when an older
  middleware sends an integration this build does not recognise.
Both fetchers checked `response.ok` before parsing the body, but the
middleware's `sendError` always pairs `ok:false` with an HTTP 400/404, so
the real error envelope's own message was discarded in favour of a bare
"responded with status 400". Parse the body first regardless of status;
fall back to a status/unexpected-response message only when the body
turns out not to be a usable envelope at all.

- packages/app/src/connection/metro-target-resolution.ts and
  packages/cli/src/commands/dev-servers.ts: reorder validation so a valid
  ok:false envelope's error.message always wins, and update their tests
  to exercise the real ok:false + HTTP 400 shape plus a 404/non-JSON body.
- dev-servers.ts: formatNoTargetsMessage's non-unreachable branch now
  ends with an actionable "pass --port" suffix instead of bare details.
- metro-target-resolution.test.ts: drop a duplicated test case.
- device-connection.test.ts: give mocked targets a device-local pageId
  (derived from their own webSocketDebuggerUrl) instead of defaulting to
  the composite id, and add an end-to-end reconnect test where the
  preferred page is second in the list.
- open-command.test.ts: fixtures use device-local pageId values instead
  of composite ids, matching the new invariant.
- logical-device-id.ts: fix a comment quoting the old, now-incorrect
  error text.
@V3RON
V3RON force-pushed the feat/unified-target-discovery branch from 6e0d435 to 0f37a7f Compare September 3, 2026 08:24
@V3RON
V3RON merged commit 3c4905f into main Sep 3, 2026
4 checks passed
@V3RON
V3RON deleted the feat/unified-target-discovery branch September 3, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tech debt: Metro target discovery is implemented three times (middleware, CLI, app)

1 participant