Skip to content

feat: extract a host integration contract behind @rozenite/integration - #458

Draft
V3RON wants to merge 3 commits into
mainfrom
claude/rozenite-integration-package
Draft

feat: extract a host integration contract behind @rozenite/integration#458
V3RON wants to merge 3 commits into
mainfrom
claude/rozenite-integration-package

Conversation

@V3RON

@V3RON V3RON commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Description

Draft / WIP — opened for direction on the contract's shape before it grows further. Stacked on #456.

@rozenite/middleware decided what to do by asking "is this Lynx?" in nine places. Behind those conditionals sat four real differences between the hosts Rozenite serves, so this gives them a contract and two implementations.

  • New @rozenite/integration defines RozeniteIntegrationProvider — an id, an environment preflight, a debugger frontend path (or null), and whatever hooks a bundler's dev server needs — and ships the React Native implementation. That implementation absorbs the React Native resolution, the version check and the two @react-native/dev-middleware patches, which all move out of the middleware.
  • @rozenite/lynx-dev implements the Lynx side, where all four members are no-ops or null.
  • initializeRozenite takes the implementation, not a string it branches on, defaulting to React Native so @rozenite/metro and @rozenite/repack are untouched. The unreleased integration config value has no setter left, so it is removed; everything that read it now reads integration.id.

Net 939 deletions against 131 insertions in the tracked diff, plus the new package.

Related Issue

Part of #455 — the abstraction the compatibility work kept pointing at.

Context

The acceptance test was deleting conditionals, not renaming them. Three of the nine collapse into if (debuggerFrontendPath) — "does this integration have a frontend to serve?" — which is the shape that says the contract is real rather than an isLynx in a new coat. No isLynx-style branch survives in the middleware.

What is deliberately not in the contract. No source-map or symbolication member: it lands with its first caller, the same discipline applied to the CDP domain in #456 rather than building a dispatch table for one method. packages/middleware/src/agent/** is untouched, so #448's capability profiles can be reconciled on their own terms once that merges — they are arguably a fifth member of this contract, but that is a follow-up, not a guess made here.

Known open questions for review — the reason this is a draft:

  • verifyEnvironment preserves verifyReactNativeVersion's current behaviour, which exits the process on a version mismatch rather than throwing. The contract documents that rather than quietly changing process-exit semantics inside a refactor, but a thrown error is probably the better contract and would be a small follow-up.
  • One diagnostic log line (Resolved react-native to: …) was dropped rather than grow the interface for it.
  • @rozenite/tools now exports RozeniteLogLevel alongside the logger it already exported, so the new package reuses that logger instead of adding a third copy of it.

Testing

From the repository root, after git fetch origin main:

  • pnpm checks:affected — passed
  • pnpm test:affected — 65/65 tasks

integration-domain.test.ts and integration-domain.e2e.test.ts moved to the new package with their assertions unchanged. The end-to-end test was re-confirmed after the move to fail when the dev-middleware patch is disabled, so it still fails for the reason it exists. middleware.test.ts's Lynx suite now builds a small fake implementation of the contract instead of passing { integration: 'lynx' }, which is a fair proxy for how a third host would arrive.

Not verified on a device: no Lynx app or React Native device was exercised; the Lynx implementation is all no-ops, and the React Native one is covered by the moved suites plus a new contract test.


Generated by Claude Code

claude added 3 commits August 22, 2026 18:08
Rozenite now runs in three integrations, and a plugin built for one can
load into another and break. This lays the producer side of a compatibility
gate: plugins declare what they support, and the dev server can say what a
given target actually is.

`@rozenite/tools` gains the shared vocabulary — `react-native`,
`react-native-web`, `lynx`, `lynx-web` — plus `resolveIntegration`, which
combines the host a dev server serves with the device platform CDP reports
for a target. That combination is what makes one Metro serving a native app
and a browser tab at once resolvable: the host alone cannot answer it.

`@rozenite/middleware` answers a `Rozenite.getEnvironment` CDP domain from
inside the inspector proxy, by patching `createDevMiddleware` and composing
with whatever `unstable_customInspectorMessageHandler` the embedder already
passes (Expo passes one). The domain is answered locally and never
forwarded — a device has no such domain and would reply -32601. Every
failure mode degrades to a warning and the config-supplied host integration,
which is also served through `/rozenite/app/config` and `__ROZENITE__` as
the pre-handshake fallback.

`@rozenite/vite-plugin` gains `integrations` in `rozenite.config.ts`,
validated at config load so a typo fails the build, and always emitted into
`rozenite.json` so a manifest answers the question even when the field is
omitted.

The middleware's `platform` option is renamed to `integration` while it is
still unreleased, so it stops colliding with the device-OS meaning
`platform` carries everywhere else.

Nothing enforces compatibility yet — consumption follows separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyZswYgjDu7i6zoeFct8kX
Follow-up cleanup on the previous commit, no behavior change.

- Extract `requireDevMiddlewareInternal` into `resolve.ts`. Reaching past
  `@react-native/dev-middleware`'s `exports` map to require one of its
  internal modules is the version-sensitive part of both patches into that
  package, and it now lives in one place instead of two copies that could
  drift.
- Drop two try/catch blocks in the CDP message handlers whose catch could
  never fire: reading a frame is total on its own, since `asRecord` narrows
  anything the wire can carry and every access after it is optional-chained.
  The remaining guards cover the two things that genuinely throw — code we
  don't own, and the reply send.
- Narrow the local mirror of dev-middleware's connection type to the one
  member this code touches.
- Give `getEntryPointHTML` an options object. It had grown to six
  positional parameters, several of them same-typed and optional, where a
  transposition would still type-check.
- Drop `ROZENITE_INTEGRATION_LABEL`, which nothing reads yet; it belongs in
  the change that first displays a label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyZswYgjDu7i6zoeFct8kX
WIP. The middleware decided what to do by asking "is this Lynx?" in nine
places. Behind those conditionals were four real differences between the
hosts Rozenite serves, so this gives them a contract and two
implementations.

`@rozenite/integration` defines `RozeniteIntegrationProvider` — an id, an
environment preflight, a debugger frontend path (or `null`), and whatever
hooks the bundler's dev server needs — and ships the React Native
implementation, which absorbs the React Native resolution, version check
and the two `@react-native/dev-middleware` patches that used to live in the
middleware. `@rozenite/lynx-dev` implements the Lynx side, where all four
are no-ops or `null`.

`initializeRozenite` now takes the implementation rather than a string it
branches on, defaulting to React Native so `@rozenite/metro` and
`@rozenite/repack` are unchanged. The unreleased `integration` config value
has no setter left, so it is gone; everything that read it now reads
`integration.id`.

All nine conditionals are gone rather than renamed: three of them collapse
into "does this integration have a frontend to serve?", which is the test
of whether the contract earns its place.

Still to come, deliberately not here: the source-map member lands with its
first caller, and PR #448's capability profiles are reconciled once that
merges.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VyZswYgjDu7i6zoeFct8kX
@V3RON
V3RON force-pushed the claude/plugin-integration-compatibility-yj0odc branch from 2cc6fd4 to bd7de21 Compare August 31, 2026 08:22
Base automatically changed from claude/plugin-integration-compatibility-yj0odc to main August 31, 2026 09:27
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.

2 participants