feat(lynx): guarantee Rozenite plugins never enter Lynx production bundles - #494
Open
V3RON wants to merge 5 commits into
Open
Conversation
@rozenite/lynx splits into a side-effect-free `.` seam (<Rozenite />, rendering a statically-imported dev-entry noop) and a `./runtime` subpath carrying the device runtime that used to be the root export. rozeniteLynxPlugin drops apply: 'serve' and installs the shared RozeniteResolverPlugin (from @rozenite/middleware) unconditionally, in both rspeedy dev and rspeedy build, so a production build now fails, naming the importing file, if it resolves into a Rozenite plugin package through anything other than a declared production entry -- the same guarantee @rozenite/repack gives React Native. @rozenite/middleware grows an integration check alongside the existing production-entry guard: a plugin resolved into a Lynx bundle must declare "lynx" in its manifest's `integrations`, or the build fails the same way. The seam's dev-entry-redirect matching is generalized from a single hardcoded package name to a set covering both @rozenite/react-native and @rozenite/lynx, and extended to match the .cjs specifier Lynx's Rollup-bundled CJS build emits (its tsc-built React Native counterpart never rewrites the extension, so this case had no test coverage before). apps/playground-lynx migrates to the new convention: <Rozenite /> mounted once at the app root, every plugin panel moved under a rozenite.dev/ directory so RozeniteResolverPlugin's dev-time advisory is correctly suppressed for them (any file under that directory counts as the dev entry, not only the entry file itself).
… end-to-end @rozenite/test-utils gains bundleLynxForRelease, the rspeedy/rspack counterpart to bundleForRelease: it builds a throwaway Lynx app through rspeedy's JavaScript API (createRspeedy + .build()) in production mode and reports which modules rspack actually put in the compilation, via a small rspack plugin tapping compiler.hooks.afterCompile -- the same non-vacuous, module-graph-based signal the Metro bench uses, not a grep over emitted source. .build() rejects with a generic "Rspack build failed." on a compile error rather than the real message, so the bench also captures stats.errors through onAfterBuild and re-throws with it, letting a caller assert on RozeniteResolverPlugin's actual message the way the Metro bench's callers do. packages/lynx/src/__tests__/release-bundle.test.ts exercises rozeniteLynxPlugin through six real rspeedy builds: a direct plugin import fails naming the file, a plugin that doesn't declare Lynx support fails naming the integrations it does declare, a declared production entry succeeds with zero panel code, the guard still fires when the plugin is disabled, allowInProduction bypasses it, and a clean app ships zero Rozenite modules.
…undle bench Records the resolved JSX-runtime question (@lynx-js/react does not depend on react at all, so the seam is built against its own jsx-runtime rather than shared source with @rozenite/react-native) and documents bundleLynxForRelease alongside the existing Metro bench in docs/agents/release-bundle-testing.md.
An adversarial review of the previous two checkpoints found a real
bug: the seam's CJS build (dist/index.cjs) required its dev entry
without any interop, so once RozeniteResolverPlugin redirected the
request to the app's rozenite.dev.tsx (compiled independently by
rspack/webpack as { default, __esModule: true }), it rendered that
namespace object instead of the component inside it. Two follow-up
fixes were tried and reverted before landing on the real one -- see
vite.seam.config.ts's comment for why a same-build entry and a
namespace-import unwrap both made things worse. The fix: build
dev-entry.tsx as a genuinely separate Rollup output
(vite.dev-entry.config.ts) and mark it external to the seam build,
with a per-format output.paths remap (the flat dist/ layout needs
dev-entry.cjs in the CJS build, not the literal dev-entry.js source
specifier) and explicit interop: 'auto' so Rollup's own runtime
__esModule check handles both shapes. Verified against both shapes
directly through the built dist/index.cjs, and locked in with an
automated regression test.
Also, from the same review:
- The integration-mismatch error now names the allowInProduction
escape hatch, matching the existing production-entry error's shape.
- rozeniteLynxPlugin now derives lynx vs lynx-web from the Rsbuild
environment name instead of always targeting "lynx".
- The release-bundle suite gained the ADR's actual headline case: a
clean app with a real rozenite.dev.tsx present, proving production
never reaches it, including with enabled forced on.
- packages/lynx/README.md and apps/playground-lynx/README.md drift
(still describing the pre-seam src/plugins/ layout) is fixed.
- ADR 0002 now honestly defers `rozenite init` Lynx scaffolding
(packages/cli is entirely React-Native-shaped today) as tracked
follow-up work rather than claiming it as done.
V3RON
force-pushed
the
claude/lynx-rozenite-seam-adr-1134d0
branch
from
September 4, 2026 08:43
060896a to
51d24fa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR 0002 (
docs/adr/0002-lynx-plugins-never-enter-production-bundles.md), extending the production guarantee #445 gives Metro/Re.Pack to Lynx:@rozenite/lynxgets an app-side seam, mirroring@rozenite/react-native's: a side-effect-free<Rozenite />component (the new.export) that redirects torozenite.dev.tsxin development and resolves to a shipped noop in production. The device runtime that used to be the root export moved to@rozenite/lynx/runtime(breaking change, covered in the changeset).rozeniteLynxPlugindropsapply: 'serve'and installs the sharedRozeniteResolverPlugin(from@rozenite/middleware) unconditionally, in bothrspeedy devandrspeedy build— a production build now fails, naming the importing file, if it resolves into a Rozenite plugin package through anything other than a declared production entry.@rozenite/middlewaregrows an integration check: a plugin resolved into a Lynx bundle must declare"lynx"(or"lynx-web", derived from the Rsbuild environment name) in its manifest'sintegrations, or the build fails the same way. The seam-matching logic generalizes from a single hardcoded package name to cover both React Native and Lynx seams.@rozenite/test-utilsgainsbundleLynxForRelease, the rspeedy/rspack counterpart to the existing MetrobundleForReleasebench, andpackages/lynxgets a 7-case release-bundle suite proving the guarantee end-to-end against realrspeedy buildruns — including the ADR's headline case: a clean app with a realrozenite.dev.tsxpresent, proving production never reaches it.apps/playground-lynxmigrates to the new convention:<Rozenite />mounted once at the app root, every plugin panel moved under arozenite.dev/directory.An adversarial review (Opus) of the first pass caught a real bug — the seam's CJS build required its dev entry with no interop, so a redirect to the app's
rozenite.dev.tsxrendered the wrong thing — fixed and locked in with a regression test; see the last commit for details. The review also flagged thatrozenite initLynx scaffolding, mentioned in the ADR, is a substantially separate CLI feature (Lynx-project detection, an rspeedy config wrapper) that doesn't exist for Lynx at all today; that's called out explicitly in the ADR as deferred, tracked follow-up work rather than folded into this change.Test plan
pnpm checks:affected(typecheck, lint, format) — cleanpnpm test:affected— clean, including 109 lynx tests and 280 middleware testsrspeedy buildonapps/playground-lynx: 88.7 kB bundle, zero plugin/rozenite.devstringsrspeedy dev: dev-entry redirect fires, devices connect, no spurious advisories