feat(lynx): add production Module Federation support#4907
feat(lynx): add production Module Federation support#4907ScriptedAlchemy wants to merge 46 commits into
Conversation
🦋 Changeset detectedLatest commit: ac671ad The changes in this PR will be included in the next version bump. This PR includes changesets to release 49 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@module-federation/devtools
@module-federation/cli
create-module-federation
@module-federation/dts-plugin
@module-federation/enhanced
@module-federation/error-codes
@module-federation/esbuild
@module-federation/lynx
@module-federation/managers
@module-federation/manifest
@module-federation/metro
@module-federation/metro-plugin-rnc-cli
@module-federation/metro-plugin-rnef
@module-federation/metro-plugin-rock
@module-federation/modern-js
@module-federation/modern-js-v3
@module-federation/native-federation-tests
@module-federation/native-federation-typescript
@module-federation/nextjs-mf
@module-federation/node
@module-federation/observability-plugin
@module-federation/playground
@module-federation/retry-plugin
@module-federation/rsbuild-plugin
@module-federation/rspack
@module-federation/rspress-plugin
@module-federation/runtime
@module-federation/runtime-core
@module-federation/runtime-tools
@module-federation/sdk
@module-federation/storybook-addon
@module-federation/third-party-dts-extractor
@module-federation/treeshake-frontend
@module-federation/treeshake-server
@module-federation/typescript
@module-federation/utilities
@module-federation/webpack-bundler-runtime
@module-federation/bridge-react
@module-federation/bridge-react-webpack-plugin
@module-federation/bridge-shared
@module-federation/bridge-vue3
@module-federation/inject-external-runtime-core-plugin
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b6d364925
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
🚀 Preview Environment Ready!
Details:
|
Bundle Size Report16 package(s) changed, 27 unchanged. Package dist + ESM entry
Bundle targets
Tree-shakable entrypoints
Consumer scenarios
Total dist (raw): 35.95 MB (+154.6 kB (+0.4%)) Bundle sizes are generated with rslib (Rspack). Package-root metrics preserve the historical report. Tracked subpath exports such as |
# Conflicts: # pnpm-lock.yaml
# Conflicts: # pnpm-lock.yaml
Description
This PR proposes and implements first-class Module Federation support for official Rspeedy + ReactLynx applications through a new
@module-federation/lynxpackage.The adapter preserves both normal compiled imports (
import('catalog/Card')) and the runtime API (loadRemote('catalog/Card')). Remotes are addressed through the standardmf-manifest.json; the manifest points to a public.lynx.bundlecontainer, and exposed modules remain independently HTTP-loadable lazy bundles by default. The reference Catalog is also runnable as a complete standalone native/Web product from the same UI source.This is an implementation RFC. The sections below define the transport, layer/share semantics, artifact model, compatibility constraints, test evidence, rejected alternatives, and rollout boundaries.
Related Issue
No linked issue yet. The architectural proposal and its executable reference implementation are captured in this PR for maintainer discussion.
RFC: motivation
Lynx is not a browser and cannot use DOM script injection as its remote-entry transport. It also has two JavaScript realms (background and main thread), an official binary bundle format, and a lazy-bundle loader whose registration contract differs from Webpack's browser and Node chunk loaders.
A Lynx integration therefore needs to:
.lynx.bundleby default;script,module, CommonJS, and other runtime-core remote types unchanged.Goals
import()and runtimeloadRemote().<lynx-view>browser E2E.Non-goals
remoteEntry.jsas a public deployment endpoint.Proposed architecture
The adapter is registered after the official Lynx DSL plugin. It consumes the DSL's exposed
LAYERS.BACKGROUND,LAYERS.MAIN_THREAD, andLynxTemplatePlugincontracts instead of hard-coding internal layer strings.At compile time it:
.lynx.bundle;metaData.remoteEntryto that public bundle;program without adding another public container.
official
react:main-threadsuffix also prevents Rspeedy from applying itsbackground
ttwrapper to native main-thread roots.Standalone iOS shell
The reference iOS application is derived from the official Lynx
ios/HelloLynxSwiftUIKit starter at commitf8230ca6aa1c9e629e30272971d0c03450b13e8e. It embedsLynxView, initializesLynxEnv, uses the official CocoaPods distribution, and does not depend onLynx Explorer.
Release embeds only the host
main.lynx.bundle; it does not package the remotecontainer, lazy exposes, or shared remote fallbacks into the IPA. The host is
built with its production manifest origin before the sync step. Both build
configurations accept
LYNX_BUNDLE_URLfor Simulator/LAN development. Releasepermits only local networking; arbitrary and public insecure HTTP loads remain
disabled.
Physical-device development uses one
LYNX_REMOTE_ORIGINfor the host'scompiled manifest URL and
LYNX_BUNDLE_URLfor the root bundle. Theios:devicecommand rejects loopback origins and binds Rspeedy to0.0.0.0,preventing the common failure where the phone is accidentally directed to its
own localhost.
The shell implements both current resource seams.
LynxTemplateResourceFetcherloads Bundle and Lazy Bundle bytes, while
LynxGenericResourceFetchercoversgeneral/external resources. A legacy
LynxTemplateProviderimplementation isretained for SDK compatibility. HTTP failures return status-bearing errors and
all URL-session requests expose cancellation blocks to Lynx.
Local file requests are restricted to the signed app bundle and a shell-owned
temporary cache. Path-based resources preserve extensions and relative bundle
directories, cache the latest path by URL, enforce a 64 MiB aggregate budget
across all immutable retained paths, and remove the cache only with the fetcher so
paths already returned to Lynx remain valid.
Remote responses use delegate-only download tasks with request and resource
timeouts; expected or received bytes above 64 MiB cancel in flight before the
temporary download can grow without bound.
Runtime request path
sequenceDiagram participant App as Host import/loadRemote participant MF as Federation runtime participant Manifest as mf-manifest.json participant Lynx as Lynx runtime API participant Entry as container.lynx.bundle participant Lazy as expose lazy bundle App->>MF: import("catalog/Card") or loadRemote("catalog/Card") MF->>Manifest: resolve remote metadata Manifest-->>MF: type="lynx", remoteEntry URL MF->>Lynx: fetchBundle(entry URL) Lynx-->>MF: binary bundle MF->>Lynx: loadScript(bundle section) Lynx-->>MF: Module Federation container MF->>Lynx: loadLazyBundle(expose URL) Lynx-->>MF: ids + modules + runtime MF->>MF: install into calling webpack runtime MF-->>App: exposed module factory/exports@module-federation/lynx/runtimePluginhandlesremoteEntry.type: 'lynx'and.lynx.bundleURLs. Explicit background-only JavaScript entries can usetype: 'lynx-js'andlynx.requireModuleAsync. All unrelated remote types returnundefinedfrom the hook and continue through their existing loaders.Entry loads are cached by URL, global name, and realm. Rejected or timed-out promises are evicted, allowing later retries. The default timeout is 30 seconds and is configurable through
runtimePluginOptions.timeout.The lazy-chunk adapter preserves the official ReactLynx synchronous thenable when a cached DynamicComponent and all of its shared consumes install in the same turn. This is required by the first-render
lazy()shortcut. Network loads and pending shared consumes use ordinary timeout-bound promises instead.Manifest contract
Applications configure the manifest, not the generated JavaScript container:
The manifest remains the standard Module Federation control plane. Its
metaData.remoteEntryis rewritten to:{ "name": "catalog.native.lynx.bundle", "type": "lynx" }The JavaScript container emitted by Rspack is an encoder input, not a public remote endpoint. This keeps discovery, versioning, and runtime integration compatible with the existing manifest pipeline while allowing a Lynx-specific transport.
Web remotes use
publicPath: 'auto'. Browser-like hosts infer a root-relative manifest base from the fetchedResponse.url; Node hosts use an absolute manifest URL or an equivalent custom fetch response. The Lynx runtime plugin separately resolves lazy bundle URLs from the resolved container URL because Lynx Web evaluation cannot rely ondocument.currentScript. Native remotes use an absolute Rspeedyoutput.assetPrefixfrom the deployment origin because Lynx's native lazy-bundle bootstrap also consumes Webpack's public path.Artifact and chunking model
remoteBundle.chunkingsupports:split(default).lynx.bundleplus independently fetched lazy.bundlefilessingle.lynx.bundlecontaining background-only module chunkssplitis the safe federation default. It prevents every expose and shared fallback from being shipped in the entry binary, preserves on-demand transport, and gives each ReactLynx exposure its own paired lazy roots. Native TASM encodes the background module and main-thread snapshot program into the same DynamicComponent.bundle; the enhanced federation container itself remains background-only.Web remotes reject
singlebecause one external bundle has only one main-thread root and cannot activate independently scoped ReactLynx exposure roots. Nativesingleis restricted to background-only modules; ReactLynx UI exposures remain split.Ordinary Rspeedy entry outputs are preserved by default. Dedicated remote builds can set
preserveSourceEntryBundles: falseto publish only the federation manifest, container, and lazy bundles.Layers, issuer layers, and shares
One neutral container carries internal aliases for layer-specific expose implementations; a second container is unnecessary.
Split remote bundles and hosts with
mainThread: truecompile exposes for both issuer layers. Shared declarations are normalized into realm-qualified scopes. Compiler layers and runtime realms are deliberately separate axes:adapterOptions.layercontrols Rspacklayer/issuerLayer, while share-scope keys remain qualified by the semantic exposedLAYERS.BACKGROUNDorLAYERS.MAIN_THREADrealm so runtime routing is stable even with custom compiler layers. The public API usesrealm: 'background' | 'main-thread'; application configuration does not depend on private DSL layer names. Background-only hosts and nativesingleremotes initialize only the background scope and reject main-thread shares at configuration time. Native split mode uses the main-thread alias to generate snapshot bytecode inside each lazy bundle, not to launch a second native federation runtime.A singleton is unique within one JavaScript realm, share scope, and share key. The host, compiled imports, runtime API consumers, and remotes can share the same stateful instance in the background realm. The main-thread realm intentionally receives a different instance.
The implementation does not recommend partially sharing ReactLynx internals:
@lynx-js/reactalone does not cover its/internal, JSX-runtime, Lepus, and lazy-runtime subpaths. Application state and ordinary libraries are safer singleton candidates.Public API
Host configuration:
Compiled import:
Runtime API:
Remote configuration:
Reference application
apps/lynx-module-federation-demois an official Rspeedy + ReactLynx application, not a browser-only simulation.It includes:
release/debug ATS split, lifecycle forwarding, and embedded-host sync;
.lynx.bundlebuilds;pluginQRCode()development path for Lynx Explorer on a phone;<lynx-view>element;import()and runtimeloadRemote()consumers;The Linux native job builds real launchable artifacts, decodes their binary
headers, validates manifest/layer/share metadata, and verifies all HTTP
resources. It uploads the exact host and remote artifacts to a
macos-15job.That job resolves the locked official Lynx 3.9.0 pods, builds and launches one
Release app on an iPhone Simulator, then runs the federated host, standalone
Catalog, and embedded-host UI scenarios against that binary. It taps Load
remote catalog, waits for the ready-only native accessibility identifier that
is mounted after compiled imports, runtime loading, and singleton identity all
pass, and asserts requests for the host, manifest, container, and all three lazy
expose bundles. The harness creates and deletes a disposable simulator rather
than changing a developer's existing simulator state. Successful runs retain
the screenshot and request trace; failed runs additionally retain the single
.xcresultbundle. The macOS invocation also runs four XCTest resource-boundary cases in the same Release build. The job caches lockfile-validated Bundler/CocoaPodsmaterial, leaves Xcode
DerivedDatauncached, disables only the CI-irrelevantcompiler index store, and avoids compiling Lynx twice under Debug and Release.
This matches Lynx upstream's safe cache/build boundary and is native iOS
runtime evidence, not an Explorer or browser proxy.
The Web E2E executes the official runtime in Chromium with a mobile viewport and touch input. It verifies rendering, navigation, both import styles, all HTTP requests, shared-state identity/mutation, and the absence of Lynx/page/console errors.
Compatibility changes
The embedded runtime initializers replace logical assignment syntax (
??=,||=) with equivalent explicit assignments and useObject.prototype.hasOwnProperty.call. This keeps the emitted runtime parseable by ES2019 and Lynx TASM consumers without changing the public runtime API.The demo currently pins the layers-capable Rspack canary
2.1.5-canary-54a0d8f3-20260715194831. A Node 24registerHooksshim isolates that canary to Rspeedy's Rspack resolution so the monorepo's normal toolchain is not globally replaced.The compatibility shim is now one documented, mechanically enforced boundary owned by every demo build/dev/preview script. It can be deleted atomically when Rspeedy accepts the repository Rspack package directly. The final architecture pass also moves remote-bundle state to per-compilation stores, models lazy loading and demo loading as retryable controllers, isolates E2E output/serving, and decomposes the iOS URL, download, storage, and Lynx adapter boundaries.
Failure behavior and safety
now evicts rejected entry promises generically so any transport can retry.
Alternatives considered
Two federation containers
Rejected. Layers and issuer layers already select the correct expose and share implementation. Two containers would duplicate negotiation, caching, manifests, and failure states while still not creating cross-thread object identity.
Put all remote code in one
.lynx.bundleSupported only as native background-only
singlemode. It is not the default because it defeats independent exposure transport, cacheability, and shared-fallback avoidance; it cannot represent independently activated Web main-thread UI roots.Address
remoteEntry.jsdirectlyRejected. It exposes an intermediate encoding artifact and bypasses the standard manifest discovery contract.
Add Lynx branches throughout runtime-core
Rejected. A runtime plugin owns platform entry loading and chunk registration. Runtime-core remains transport-neutral: it makes failed entry-cache loads retryable, lets preload plugins explicitly abstain, and preserves the fetched manifest URL for standard
publicPath: autoinference. No Lynx type or loader is added to core.Use only runtime
loadRemote()Rejected. Existing application ergonomics depend on compiled federated imports, so both paths are first-class and covered by E2E.
Validation
@module-federation/lynx: 121 tests passed across compiler normalization, artifact construction, entry transport, runtime plugins, and native/Web chunk loading.@module-federation/runtime-core: 98 tests passed, including root-relative manifestpublicPath: autoinference, stale-registration isolation, and retry after manifest-load failure.@module-federation/sdk: 70 tests passed; 1 skipped.@module-federation/webpack-bundler-runtime: 101 tests passed.Lynx package typecheck, lint, and build passed.
Native E2E: host/remote binary decode, manifest/container validation, 3 lazy bundles, and live HTTP serving passed.
The artifact E2E decodes a real lazy bundle and verifies the DynamicComponent identity assignment remains inside Rspeedy's
tt.define(...exports...)RuntimeWrapper boundary; this guards the iOS-only top-levelexportsfailure found by the simulator.Official iOS pods resolve from the committed lockfile; the CocoaPods-integrated Xcode project parses with both application and UI-test targets.
iOS Simulator CI builds a standalone
OrbitControl.app, exercises compiled imports and runtime loading, verifies singleton identity, audits every bundle request, and launches the embedded-host Release branch.Lynx for Web E2E: federated Orbit and standalone Catalog executed in real
<lynx-view>instances with 66 HTTP requests, including the manifest, container, and three lazy bundles.Frozen lockfile install, Prettier, actionlint, and CI selector validation passed.
Final correctness/security and maintainability reviews found and resolved physical-device origin, native file-access, safe-area/rotation, artifact-hermeticity, bounded download, retry-cache, immutable iOS resource-path lifetime, manifest-request lifetime, paired-layer suffix, simulator-state, and Release-coverage issues; the final audit reported no P0-P2 findings.
Final GitHub PR matrix on
ac671aded: all checks passed, including real Lynx iOS and Metro iOS simulator E2E.Rollout and follow-ups
@module-federation/lynxas an opt-in package.Reviewer guide
packages/lynx/src/plugin.tsandpluginOptions.ts: Rspeedy/Rspack integration and normalization boundaries.packages/lynx/src/remoteBundle.ts: layer-aware container and artifact construction.packages/lynx/src/runtimePlugin.ts: remote-entry transport hook.packages/lynx/src/runtimeChunkLoading.ts: lazy-bundle installation into the caller runtime.packages/lynx/src/remoteManifest.ts: manifest rewrite/validation.apps/lynx-module-federation-demo/federation.config.mjs: host/remote reference configuration.apps/lynx-module-federation-demo/ios/: official-starter-derived standalone UIKit shell and locked pods.apps/lynx-module-federation-demo/test/ios/: native Simulator UI/runtime/request E2E.apps/lynx-module-federation-demo/test/: native artifact/HTTP and real Web E2E evidence..github/workflows/e2e-lynx.yml: CI contract.Types of changes
Checklist