Skip to content

feat(lynx): merge @rozenite/lynx-dev in and inject the device runtime - #489

Merged
V3RON merged 1 commit into
mainfrom
claude/rozenite-lynx-polish-ac864e
Sep 2, 2026
Merged

V3RON merged 1 commit into
mainfrom
claude/rozenite-lynx-polish-ac864e

Conversation

@V3RON

@V3RON V3RON commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Description

Rozenite for Lynx is now one package, and the device runtime is injected rather than hand-imported.

  • @rozenite/lynx gains a ./rspeedy export carrying the plugin previously published as @rozenite/lynx-dev. Its . export — the device runtime — is unchanged.
  • The plugin injects the device runtime into the app's bundle via source.preEntry, so there is nothing to add to src/index.tsx any more.
  • @rozenite/lynx-dev becomes a deprecated re-export shim over @rozenite/lynx/rspeedy. Existing imports keep working.

Setup drops from two installs plus a hand-written entry-point import to one install and one line in lynx.config.ts.

Related Issue

Closes #488

Context

The hand-written import was the thing worth removing. It had to be __DEV__-guarded or the dispatcher shipped to production, and packages/lynx/README.md offered the unguarded import '@rozenite/lynx'; as an equally valid alternative — which it was not. Measured on apps/playground-lynx before this change: unguarded 424.1 kB, __DEV__-guarded 420.6 kB, no import at all 420.6 kB. The guarded build was byte-identical to no import, so the guard worked; without it ~3.5 kB shipped and Object.defineProperty(globalThis, '__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__', …) actually ran in the production app.

Injection removes that failure mode structurally rather than documenting against it. The injection sits inside the plugin's setup, and apply: 'serve' already keeps setup from running during rspeedy build — Rsbuild's plugin initializer maps action: 'build' to an expected apply of 'build' and skips anything that does not match. There is no code path from the injection site to a production bundle. apply: 'serve' and the enabled default are unchanged; this change depends on both.

Three things were verified rather than assumed before the design was settled:

  • source.preEntry survives rspeedy's config pipeline and reaches the bundle.
  • The injected module is emitted into both ReactLynx layers and still gets per-layer __BACKGROUND__ dead-code elimination, despite a preEntry module having no issuer for the layer-matched loader rules to key off. This was the one thing that could have sunk the approach, so it was probed directly.
  • Rsbuild adds preEntry modules ahead of the app's entry, which satisfies the "must run before any plugin's useRozeniteDevToolsClient" ordering the docs previously pushed onto the user.

. stays the device runtime rather than becoming the plugin. Inverting them would break every existing import '@rozenite/lynx', and keeping . as the runtime matches the convention already in the repo, where a plugin exports . for device code and a subpath for its bundler half.

The merge makes express, ws and @lynx-js/debug-router-connector dependencies of the package an app installs. They never reach the bundle — nothing reachable from . imports them — so the cost is install size only. @rozenite/lynx's README no longer claims zero runtime dependencies.

RUNTIME_ENTRY resolves by package self-reference through the package's own exports map, so it needs no node_modules lookup and behaves the same symlinked or installed. It resolves the require condition, yielding dist/index.cjs; that is fine, since the value is only ever handed to Rspack as an entry path.

Two vite-plugin-dts limitations surfaced while giving one package two entries with different build shapes, and are documented inline in packages/lynx/vite.rspeedy.config.ts: lib.fileName is ignored under ssr: true, and rollupTypes: true takes its output path from package.json's top-level types field rather than the build's own entry, so it silently emits nothing for a second entry. The rspeedy entry emits un-rolled per-module declarations instead.

Testing

  • pnpm checks:affected — 96/96 tasks passed across 27 affected packages.
  • pnpm test:affected — 56/56 tasks passed. @rozenite/lynx runs 8 test files / 99 tests, which includes all 7 suites moved over from @rozenite/lynx-dev (bridge ×4, server ×2, transport ×1); none were dropped from the run.
  • pnpm release:plan — changeset registers.
  • Production bundle, from apps/playground-lynx: NODE_ENV=production npx rspeedy build → 420.6 kB with 6 occurrences of FUSEBOX_REACT_DEVTOOLS_DISPATCHER and 0 of setupRozenite — an exact match for the no-Rozenite baseline, confirming the plugin injects nothing into a production build.
  • Dev server, from apps/playground-lynx: started npx rspeedy dev, fetched main.lynx.bundle, and confirmed the runtime is present and installed — setupRozenite ×2 (one per ReactLynx layer), and the dispatcher install site appears as var r = ()=>{ globalThis.__FUSEBOX_REACT_DEVTOOLS_DISPATCHER__ == null && Object.defineProperty(...) }; r();, i.e. executing at module scope. __BACKGROUND__ does not survive into the output, confirming per-layer DCE ran on the injected module.

Setting Rozenite up for Lynx cost two installs and a hand-written import in
the app's entry point. That import had to be `__DEV__`-guarded or the
dispatcher shipped to production, and the README offered the unguarded form
as an equally valid alternative.

`@rozenite/lynx` now carries both halves: `.` stays the device runtime,
`./rspeedy` is the plugin formerly published as `@rozenite/lynx-dev`. The
plugin injects the runtime itself via `source.preEntry`, from inside a
`setup` that `apply: 'serve'` already keeps out of `rspeedy build` — so the
production leak is structurally impossible rather than documented against.

`@rozenite/lynx-dev` stays as a deprecated re-export shim.

Closes #488
@V3RON
V3RON merged commit c9c8787 into main Sep 2, 2026
4 checks passed
@V3RON
V3RON deleted the claude/rozenite-lynx-polish-ac864e branch September 2, 2026 10:23
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.

Rozenite for Lynx: one package, and inject the device runtime instead of asking the user to

1 participant