Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/lynx-host/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Local
.DS_Store
*.local
*.log*

# Dist
node_modules
dist/

# IDE
.vscode/*
!.vscode/extensions.json
.idea

# TypeScript
*.tsbuildinfo
95 changes: 95 additions & 0 deletions apps/lynx-host/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
## Lynx Module Federation Demo

This is a ReactLynx project with Module Federation support, demonstrating the `@module-federation/rspeedy-core-plugin`.

## Features

- ✅ **Lynx/Rspeedy Integration**: Built with `@lynx-js/rspeedy`
- ✅ **Module Federation**: Uses `@module-federation/rsbuild-plugin` for build-time setup
- ✅ **Runtime Plugin**: Implements `@module-federation/rspeedy-core-plugin` for native script loading
- ✅ **Error Handling**: Comprehensive error handling and fallback components
- ✅ **TypeScript**: Full TypeScript support with proper typing

## Architecture

### Build-time Configuration
- **`lynx.config.ts`**: Configures Module Federation with `pluginModuleFederation`
- **Shared Dependencies**: React, react-dom, and @lynx-js/react are shared between remotes
- **Remote Loading**: Configured to load remote applications via manifest files

### Runtime Configuration
- **`src/module-federation-setup.ts`**: Initializes Module Federation runtime
- **`RspeedyCorePlugin`**: Bridges MF runtime with Lynx's `nativeApp.loadScript()`
- **Remote Loader**: Custom component for loading and displaying remote modules

## Getting Started

First, install the dependencies:

```bash
pnpm install
```

Then, run the development server:

```bash
pnpm run dev
```

Scan the QRCode in the terminal with your LynxExplorer App to see the result.

## Module Federation Demo

The app includes an interactive demo that showcases:

1. **Plugin Status**: Shows that the rspeedy-core-plugin is active
2. **Remote Loading**: Demonstrates loading remote modules (with error handling)
3. **Fallback Behavior**: Shows how the system handles failed remote loads
4. **Native Bridge**: Explains how the plugin bridges to Lynx's native script loading

## Key Files

- **`src/module-federation-setup.ts`**: MF runtime initialization with rspeedy plugin
- **`src/components/ModuleFederationDemo.tsx`**: Interactive demo component
- **`src/components/RemoteLoader.tsx`**: Generic remote module loader
- **`lynx.config.ts`**: Build configuration with Module Federation

## Adding Remote Applications

To add actual remote applications:

1. **Update `lynx.config.ts`**:
```typescript
pluginModuleFederation({
name: 'lynx-host',
remotes: {
'my-remote': 'my-remote@http://localhost:3001/mf-manifest.json',
},
// ...
})
```

2. **Update `src/module-federation-setup.ts`**:
```typescript
const mfInstance = createInstance({
name: 'lynx-host',
remotes: [{
name: 'my-remote',
entry: 'http://localhost:3001/mf-manifest.json',
}],
plugins: [RspeedyCorePlugin()],
});
```

3. **Use in components**:
```tsx
<RemoteLoader
remoteName="my-remote"
moduleName="Button"
fallback={FallbackComponent}
/>
```

## Development

You can start editing the page by modifying `src/App.tsx`. The page auto-updates as you edit the file.
58 changes: 58 additions & 0 deletions apps/lynx-host/docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
• I dug into the dev artifacts to see what changes when we flip the Rspeedy MF plugin on.

1. Run pnpm --filter lynx-host exec rspeedy build --mode development once with the plugin enabled and once with it commented out. That writes the dev
bundles to apps/lynx-host/dist/.rspeedy/main.
2. Compare background.js between the two runs:

- Plugin enabled (apps/lynx-host/dist/.rspeedy/main/background.js, ~lines 25 530–25 610): the MF runtime injects the standard webpack JSONP loader:

**webpack_require**.hmrM = () => {
if (typeof fetch === "undefined")
throw new Error("No browser support: need fetch API");
return fetch(**webpack_require**.p + **webpack_require**.hmrF()).then(...)
};
var chunkLoadingGlobal = self["chunk_lynx_host"] = self["chunk_lynx_host"] || [];
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
chunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
self["webpackHotUpdatelynx_host"] = (chunkId, moreModules, runtime) => { ... };
That’s exactly what shows up in the Lynx runtime logs: self["webpackHotUpdatelynx_host"] ... and the fetch-based manifest loader. Lynx’s JS VM
doesn’t expose self or the browser fetch API inside the background thread, so processData, renderPage, and the hot-update globals all explode as
soon as the dev bundle executes.
soon as the dev bundle executes.

- Plugin disabled (apps/lynx-host/dist/.rspeedy/main/background.js, ~lines 20 820–20 980): the original chunk loader is still intact and uses
Lynx’s native APIs:

**webpack_require**.hmrM = function () {
return new Promise((resolve, reject) =>
lynx.requireModuleAsync(**webpack_require**.p + **webpack_require**.hmrF(), (err, ret) => {
if (err) return reject(err);
resolve(ret);
})
)['catch']((err) => { if (err.code !== 'MODULE_NOT_FOUND') throw err; });
};
**webpack_require**.hmrI.require = function(...) { ... }
**webpack_require**.hmrC.require = function(chunkIds, ...) { ... }

No JSONP callback, no self["chunk_lynx_host"], and HMR updates flow through lynx.requireModuleAsync, which is why dev mode works when the plugin
is off.

So the regression isn’t the shared modules themselves—it’s that the stock ModuleFederationPlugin swaps in the browser chunk/HMR runtime (JSONP + self +
fetch) even though Rspeedy’s target has chunkLoading: 'lynx'. We need the Rspeedy-specific plugin to preserve the Lynx chunk loader and manifest loader
instead of letting the MF runtime fall back to JSONP.

Next steps to unblock dev:

1. Adjust the new @module-federation/rspeedy-plugin so that, after it calls pluginModuleFederation, it forces the Rspack config back to the Lynx chunk
loader. The simplest fix is to hook api.modifyRsbuildConfig and, for each environment, set:

bundlerConfig.output.chunkLoading = 'lynx';
bundlerConfig.output.hotUpdateGlobal = undefined;
bundlerConfig.plugins = bundlerConfig.plugins?.filter(p => p.name !== 'JsonpTemplatePlugin');
and re-inject the Lynx HMR runtime if necessary (the code that emits lynx.requireModuleAsync lives in the original Rspeedy template).
and re-inject the Lynx HMR runtime if necessary (the code that emits lynx.requireModuleAsync lives in the original Rspeedy template).

2. Alternatively, tap into api.modifyBundlerChain and override the target to lynx-main/lynx-background before Federation runs so it can’t stomp the
loader.

Once we keep the custom chunk loader in place, the dev bundle will go back to using lynx.requireModuleAsync and the self/fetch errors disappear.
45 changes: 45 additions & 0 deletions apps/lynx-host/lynx.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { defineConfig } from '@lynx-js/rspeedy';

import { pluginQRCode } from '@lynx-js/qrcode-rsbuild-plugin';
import { pluginReactLynx } from '@lynx-js/react-rsbuild-plugin';
import { pluginTypeCheck } from '@rsbuild/plugin-type-check';
import { pluginModuleFederationRspeedy } from '@module-federation/rspeedy-plugin';

export default defineConfig({
server: {
host: '10.210.20.64',
},
dev: {
// Lynx main thread doesn't have `self`; disable HMR runtime to avoid `webpackHotUpdate` injections.
hmr: false,
},
plugins: [
pluginQRCode({
schema(url) {
// We use `?fullscreen=true` to open the page in LynxExplorer in full screen mode
return `${url}?fullscreen=true`;
},
}),
pluginReactLynx(),
// Test with remotes and shared config
pluginModuleFederationRspeedy({
name: 'lynx_host',
remotes: {
lynx_remote: 'lynx_remote@http://localhost:3001/mf-manifest.json',
},
// shared: {
// '@lynx-js/react': {
// singleton: true,
// eager: false,
// },
// },
dts: false,
dev: {
disableDynamicRemoteTypeHints: true,
disableLiveReload: true,
disableHotTypesReload: true,
},
}),
// pluginTypeCheck(), // Temporarily disabled for MF testing
],
});
34 changes: 34 additions & 0 deletions apps/lynx-host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "lynx-host",
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "rspeedy build",
"dev": "rspeedy dev",
"preview": "rspeedy preview",
"test": "vitest run"
},
"dependencies": {
"@lynx-js/react": "^0.112.6",
"@module-federation/enhanced": "workspace:*",
"@module-federation/rspeedy-core-plugin": "workspace:*",
"@module-federation/rspeedy-plugin": "workspace:*"
},
"devDependencies": {
"@lynx-js/preact-devtools": "^5.0.1-6664329",
"@lynx-js/qrcode-rsbuild-plugin": "^0.4.1",
"@lynx-js/react-rsbuild-plugin": "^0.10.13",
"@lynx-js/rspeedy": "^0.11.1",
"@lynx-js/types": "3.4.11",
"@module-federation/rsbuild-plugin": "workspace:*",
"@rsbuild/plugin-type-check": "1.2.4",
"@testing-library/jest-dom": "^6.8.0",
"@types/react": "^18.3.23",
"jsdom": "^26.1.0",
"typescript": "~5.9.2",
"vitest": "^3.2.4"
},
"engines": {
"node": ">=18"
}
}
119 changes: 119 additions & 0 deletions apps/lynx-host/src/App.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
:root {
background-color: #000;
--color-text: #fff;
}

.Background {
position: fixed;
background: radial-gradient(
71.43% 62.3% at 46.43% 36.43%,
rgba(18, 229, 229, 0) 15%,
rgba(239, 155, 255, 0.3) 56.35%,
#ff6448 100%
);
box-shadow: 0px 12.93px 28.74px 0px #ffd28db2 inset;
border-radius: 50%;
width: 200vw;
height: 200vw;
top: -60vw;
left: -14.27vw;
transform: rotate(15.25deg);
}

.App {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

text {
color: var(--color-text);
}

.Banner {
flex: 5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
}

.Logo {
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}

.Logo--react {
width: 100px;
height: 100px;
animation: Logo--spin infinite 20s linear;
}

.Logo--lynx {
width: 100px;
height: 100px;
animation: Logo--shake infinite 0.5s ease;
}

@keyframes Logo--spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@keyframes Logo--shake {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}

.Content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

.Arrow {
width: 24px;
height: 24px;
}

.Title {
font-size: 36px;
font-weight: 700;
}

.Subtitle {
font-style: italic;
font-size: 22px;
font-weight: 600;
margin-bottom: 8px;
}

.Description {
font-size: 20px;
color: rgba(255, 255, 255, 0.85);
margin: 15rpx;
}

.Hint {
font-size: 12px;
margin: 5px;
color: rgba(255, 255, 255, 0.65);
}
Loading
Loading