Skip to content

Commit 9ef3c07

Browse files
feat: single runtime example
1 parent 908450d commit 9ef3c07

File tree

7 files changed

+260
-122
lines changed

7 files changed

+260
-122
lines changed
Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,48 @@
1-
# Offline Remotes Runtime Plugin
1+
# Single Runtime Plugin Example
22

3-
This demo boots only app1, app2 remains offline.
4-
Loading localhost:3001 will render a error message from the runtime plugin as a react module when the container if offline.
3+
This example demonstrates how the Module Federation single runtime plugin works to ensure shared dependencies use a single runtime instance when a remote application loads components from its host.
4+
5+
## Running the Demo
6+
7+
1. Start both applications:
8+
```bash
9+
# In app1 directory
10+
npm start # Runs on port 3001
11+
12+
# In app2 directory
13+
npm start # Runs on port 3002
14+
```
15+
16+
## What to Observe
17+
18+
### On App1 (port 3001)
19+
When you browse to `localhost:3001`, observe the Runtime Information section:
20+
- Notice that App2's module is using `app1_partial.js` instead of `remoteEntry.js`
21+
- This happens because App2 lists App1 as a remote, and to avoid loading conflicting runtimes from the same build (App1), the plugin switches to using the partial bundle
22+
- The partial bundle ensures App1's components use the host's runtime when loaded in App2
23+
24+
### On App2 (port 3002)
25+
When you browse to `localhost:3002`, observe the Runtime Information section:
26+
- When loading App1's remote components, it uses the standard `remoteEntry.js`
27+
- This is because App1 is not the host in this context
28+
- Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate
29+
30+
## How it Works
31+
32+
The single runtime plugin prevents runtime conflicts by:
33+
1. When a remote app loads components from its host:
34+
- The plugin detects this pattern and switches to using `{hostName}_partial.js`
35+
- This ensures the remote uses the host's runtime instead of loading a duplicate
36+
- Prevents conflicts in singleton modules and shared dependencies
37+
38+
2. When loading other remotes:
39+
- Uses the standard `remoteEntry.js`
40+
- No runtime conflict possible since it's loading from a different build
41+
42+
### Shared Dependencies
43+
Both apps share:
44+
- React (singleton)
45+
- ReactDOM (singleton)
46+
- Lodash (version matching)
47+
48+
The single runtime plugin ensures these shared dependencies maintain their singleton status by preventing duplicate runtime loading from the same build.

runtime-plugins/single-runtime/app1/rspack.config.js

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,23 @@ module.exports = {
6767
],
6868
},
6969
plugins: [
70-
//TODO: fix rspack federation plugin to create secondary container automatically
71-
// new ModuleFederationPlugin({
72-
// name: 'app1',
73-
// filename: 'remoteEntry.js',
74-
// remotes: {
75-
// app2: 'app2@http://localhost:3002/remoteEntry.js',
76-
// },
77-
// runtimePlugins: [require.resolve('./single-runtime.js')],
78-
// exposes: {
79-
// './Button': './src/Button',
80-
// },
81-
// shared: {
82-
// ...deps,
83-
// react: {
84-
// singleton: true,
85-
// },
86-
// 'react-dom': {
87-
// singleton: true,
88-
// },
89-
// lodash: {},
90-
// },
91-
// }),
92-
new ModuleFederationPlugin({
93-
name: 'app1',
70+
new ContainerPlugin({
71+
name: 'app1_partial',
9472
filename: 'app1_partial.js',
73+
library:{
74+
type: 'var',
75+
name: 'app1'
76+
},
9577
runtime: undefined,
78+
runtimePlugins: [require.resolve('./single-runtime.js')],
79+
exposes: {
80+
'./Button': './src/Button',
81+
},
82+
}),
83+
new ModuleFederationPlugin({
84+
name: 'app1',
85+
runtime: false,
86+
filename: 'remoteEntry.js',
9687
remotes: {
9788
app2: 'app2@http://localhost:3002/remoteEntry.js',
9889
},
Lines changed: 77 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,86 @@
11
import React from 'react';
2-
import ReactDOM from 'react-dom';
3-
import lodash from 'lodash';
4-
52
import LocalButton from './Button';
63
import RemoteButton from 'app2/Button';
74

8-
// A function to generate a color from a string
9-
const getColorFromString = str => {
10-
// Prime numbers used for generating a hash
11-
let primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
12-
let hash = 0;
13-
14-
// Generate a hash from the string
15-
for (let i = 0; i < str.length; i++) {
16-
hash += str.charCodeAt(i) * primes[i % primes.length];
17-
}
5+
const App = () => {
6+
const [count, setCount] = React.useState(0);
7+
8+
return (
9+
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
10+
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
11+
<h1>App 1 - Single Runtime Demo</h1>
12+
<a
13+
href="http://localhost:3002"
14+
style={{
15+
padding: '8px 16px',
16+
background: '#e24a90',
17+
color: 'white',
18+
textDecoration: 'none',
19+
borderRadius: '4px',
20+
fontSize: '14px'
21+
}}
22+
>
23+
Go to App 2 →
24+
</a>
25+
</div>
1826

19-
// Convert the hash to a color
20-
let color = '#';
21-
for (let i = 0; i < 3; i++) {
22-
const value = (hash >> (i * 8)) & 0xff;
23-
color += ('00' + value.toString(16)).substr(-2);
24-
}
27+
<div style={{
28+
padding: '15px',
29+
background: '#f8f9fa',
30+
borderRadius: '4px',
31+
marginBottom: '20px',
32+
border: '1px solid #e9ecef'
33+
}}>
34+
<h3 style={{ margin: '0 0 10px 0', color: '#4a90e2' }}>What's Happening Here?</h3>
35+
<p style={{ margin: '0', lineHeight: '1.5' }}>
36+
This is App1 running on port 3001. Notice in the Runtime Information that App2's module is using <code>app1_partial.js</code> instead
37+
of <code>remoteEntry.js</code>. This happens because App2 lists App1 as a remote, and to avoid loading conflicting runtimes from the
38+
same build (App1), the plugin switches to using the partial bundle.
39+
</p>
40+
</div>
41+
42+
<div style={{ marginBottom: '20px' }}>
43+
<h3>Shared State Counter: {count}</h3>
44+
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button>
45+
</div>
46+
47+
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
48+
<div>
49+
<h3>Local Button:</h3>
50+
<LocalButton />
51+
</div>
52+
<div>
53+
<h3>Remote Button (from App 2):</h3>
54+
<React.Suspense fallback="Loading Remote Button...">
55+
<RemoteButton />
56+
</React.Suspense>
57+
</div>
58+
</div>
2559

26-
return color;
60+
<div style={{ marginTop: '20px' }}>
61+
<h3>Runtime Information:</h3>
62+
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}>
63+
{__FEDERATION__.__INSTANCES__.map(instance => (
64+
<div key={instance.name} style={{ margin: '10px 0' }}>
65+
<div>
66+
<strong>Module: </strong>{instance.name}
67+
</div>
68+
{instance.options?.remotes?.length > 0 && (
69+
<div style={{ marginLeft: '20px', fontSize: '14px' }}>
70+
<strong>Remote Entries:</strong>
71+
{instance.options.remotes.map((remote, idx) => (
72+
<div key={idx} style={{ marginTop: '5px', color: '#666' }}>
73+
{remote.alias || remote.name}: <code>{remote.entry}</code>
74+
</div>
75+
))}
76+
</div>
77+
)}
78+
</div>
79+
))}
80+
</div>
81+
</div>
82+
</div>
83+
);
2784
};
2885

29-
// The main App component
30-
const App = () => (
31-
<div>
32-
<h1>Single Runtime</h1>
33-
<h2>Remotes currently in use</h2>
34-
{/* Display the names of the remotes loaded by the CustomPlugin */}
35-
{__FEDERATION__.__INSTANCES__.map(inst => (
36-
<span
37-
style={{
38-
padding: 10,
39-
color: '#fff',
40-
background: getColorFromString(inst.name.split().reverse().join('')),
41-
}}
42-
key={inst.name}
43-
>
44-
{inst.name}
45-
</span>
46-
))}
47-
<p>
48-
Click The second button. This will cause the <i>pick-remote.ts</i> to load remoteEntry urls
49-
from a mock api call.
50-
</p>
51-
{/* LocalButton is a button component from the local app */}
52-
<LocalButton />
53-
{/* RemoteButton is a button component loaded from a remote app */}
54-
<React.Suspense fallback="Loading Button">
55-
<RemoteButton />
56-
</React.Suspense>
57-
{/* The Reset button clears the 'button' item from localStorage */}
58-
<button
59-
onClick={() => {
60-
localStorage.clear('button');
61-
window.location.reload();
62-
}}
63-
>
64-
Reset{' '}
65-
</button>
66-
</div>
67-
);
68-
6986
export default App;
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,25 @@
11
import React from 'react';
22

3-
const style = {
4-
background: '#800',
5-
color: '#fff',
6-
padding: 12,
7-
};
3+
const Button = () => {
4+
const [clickCount, setClickCount] = React.useState(0);
5+
6+
const style = {
7+
background: '#4a90e2',
8+
color: '#fff',
9+
padding: '10px 20px',
10+
border: 'none',
11+
borderRadius: '4px',
12+
cursor: 'pointer'
13+
};
814

9-
const Button = () => <button style={style}>App 1 Button</button>;
15+
return (
16+
<button
17+
style={style}
18+
onClick={() => setClickCount(c => c + 1)}
19+
>
20+
App 1 Button (Clicks: {clickCount})
21+
</button>
22+
);
23+
};
1024

1125
export default Button;

runtime-plugins/single-runtime/app2/single-runtime.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export default function () {
22
return {
33
name: 'single-runtime-plugin',
44
init(args) {
5+
if(!__FEDERATION__.__INSTANCES__[0]) return args;
56
const hostName = __FEDERATION__.__INSTANCES__[0].name;
67
if (args.options.name !== hostName) {
78
const hostsRemote = args.options.remotes.find(remote => {
Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,85 @@
1-
import LocalButton from './Button';
21
import React from 'react';
2+
import LocalButton from './Button';
3+
import RemoteButton from 'app1/Button';
4+
5+
const App = () => {
6+
const [count, setCount] = React.useState(0);
7+
8+
return (
9+
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
10+
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
11+
<h1>App 2 - Single Runtime Demo</h1>
12+
<a
13+
href="http://localhost:3001"
14+
style={{
15+
padding: '8px 16px',
16+
background: '#4a90e2',
17+
color: 'white',
18+
textDecoration: 'none',
19+
borderRadius: '4px',
20+
fontSize: '14px'
21+
}}
22+
>
23+
← Go to App 1
24+
</a>
25+
</div>
326

4-
const RemoteButton = React.lazy(() => import('app1/Button'));
27+
<div style={{
28+
padding: '15px',
29+
background: '#f8f9fa',
30+
borderRadius: '4px',
31+
marginBottom: '20px',
32+
border: '1px solid #e9ecef'
33+
}}>
34+
<h3 style={{ margin: '0 0 10px 0', color: '#e24a90' }}>What's Happening Here?</h3>
35+
<p style={{ margin: '0', lineHeight: '1.5' }}>
36+
This is App2 running on port 3002. When loading App1's remote components, it uses the standard <code>remoteEntry.js</code> because
37+
App1 is not the host in this context. Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate.
38+
</p>
39+
</div>
40+
41+
<div style={{ marginBottom: '20px' }}>
42+
<h3>Shared State Counter: {count}</h3>
43+
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button>
44+
</div>
45+
46+
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
47+
<div>
48+
<h3>Local Button:</h3>
49+
<LocalButton />
50+
</div>
51+
<div>
52+
<h3>Remote Button (from App 1):</h3>
53+
<React.Suspense fallback="Loading Remote Button...">
54+
<RemoteButton />
55+
</React.Suspense>
56+
</div>
57+
</div>
558

6-
const App = () => (
7-
<div>
8-
<h1>API controlled remote configs</h1>
9-
<h2>App 2</h2>
10-
<LocalButton />
11-
<React.Suspense fallback="Loading Button">
12-
<RemoteButton />
13-
</React.Suspense>
14-
</div>
15-
);
59+
<div style={{ marginTop: '20px' }}>
60+
<h3>Runtime Information:</h3>
61+
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}>
62+
{__FEDERATION__.__INSTANCES__.map(instance => (
63+
<div key={instance.name} style={{ margin: '10px 0' }}>
64+
<div>
65+
<strong>Module: </strong>{instance.name}
66+
</div>
67+
{instance.options?.remotes?.length > 0 && (
68+
<div style={{ marginLeft: '20px', fontSize: '14px' }}>
69+
<strong>Remote Entries:</strong>
70+
{instance.options.remotes.map((remote, idx) => (
71+
<div key={idx} style={{ marginTop: '5px', color: '#666' }}>
72+
{remote.alias || remote.name}: <code>{remote.entry}</code>
73+
</div>
74+
))}
75+
</div>
76+
)}
77+
</div>
78+
))}
79+
</div>
80+
</div>
81+
</div>
82+
);
83+
};
1684

1785
export default App;

0 commit comments

Comments
 (0)