Skip to content

Commit

Permalink
feat: single runtime example
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptedAlchemy committed Dec 4, 2024
1 parent 908450d commit 9ef3c07
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 122 deletions.
50 changes: 47 additions & 3 deletions runtime-plugins/single-runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
# Offline Remotes Runtime Plugin
# Single Runtime Plugin Example

This demo boots only app1, app2 remains offline.
Loading localhost:3001 will render a error message from the runtime plugin as a react module when the container if offline.
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.

## Running the Demo

1. Start both applications:
```bash
# In app1 directory
npm start # Runs on port 3001

# In app2 directory
npm start # Runs on port 3002
```

## What to Observe

### On App1 (port 3001)
When you browse to `localhost:3001`, observe the Runtime Information section:
- Notice that App2's module is using `app1_partial.js` instead of `remoteEntry.js`
- 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
- The partial bundle ensures App1's components use the host's runtime when loaded in App2

### On App2 (port 3002)
When you browse to `localhost:3002`, observe the Runtime Information section:
- When loading App1's remote components, it uses the standard `remoteEntry.js`
- This is because App1 is not the host in this context
- Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate

## How it Works

The single runtime plugin prevents runtime conflicts by:
1. When a remote app loads components from its host:
- The plugin detects this pattern and switches to using `{hostName}_partial.js`
- This ensures the remote uses the host's runtime instead of loading a duplicate
- Prevents conflicts in singleton modules and shared dependencies

2. When loading other remotes:
- Uses the standard `remoteEntry.js`
- No runtime conflict possible since it's loading from a different build

### Shared Dependencies
Both apps share:
- React (singleton)
- ReactDOM (singleton)
- Lodash (version matching)

The single runtime plugin ensures these shared dependencies maintain their singleton status by preventing duplicate runtime loading from the same build.
39 changes: 15 additions & 24 deletions runtime-plugins/single-runtime/app1/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,32 +67,23 @@ module.exports = {
],
},
plugins: [
//TODO: fix rspack federation plugin to create secondary container automatically
// new ModuleFederationPlugin({
// name: 'app1',
// filename: 'remoteEntry.js',
// remotes: {
// app2: 'app2@http://localhost:3002/remoteEntry.js',
// },
// runtimePlugins: [require.resolve('./single-runtime.js')],
// exposes: {
// './Button': './src/Button',
// },
// shared: {
// ...deps,
// react: {
// singleton: true,
// },
// 'react-dom': {
// singleton: true,
// },
// lodash: {},
// },
// }),
new ModuleFederationPlugin({
name: 'app1',
new ContainerPlugin({
name: 'app1_partial',
filename: 'app1_partial.js',
library:{
type: 'var',
name: 'app1'
},
runtime: undefined,
runtimePlugins: [require.resolve('./single-runtime.js')],
exposes: {
'./Button': './src/Button',
},
}),
new ModuleFederationPlugin({
name: 'app1',
runtime: false,
filename: 'remoteEntry.js',
remotes: {
app2: 'app2@http://localhost:3002/remoteEntry.js',
},
Expand Down
137 changes: 77 additions & 60 deletions runtime-plugins/single-runtime/app1/src/App.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,86 @@
import React from 'react';
import ReactDOM from 'react-dom';
import lodash from 'lodash';

import LocalButton from './Button';
import RemoteButton from 'app2/Button';

// A function to generate a color from a string
const getColorFromString = str => {
// Prime numbers used for generating a hash
let primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
let hash = 0;

// Generate a hash from the string
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i) * primes[i % primes.length];
}
const App = () => {
const [count, setCount] = React.useState(0);

return (
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>App 1 - Single Runtime Demo</h1>
<a
href="http://localhost:3002"
style={{
padding: '8px 16px',
background: '#e24a90',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontSize: '14px'
}}
>
Go to App 2 →
</a>
</div>

// Convert the hash to a color
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ('00' + value.toString(16)).substr(-2);
}
<div style={{
padding: '15px',
background: '#f8f9fa',
borderRadius: '4px',
marginBottom: '20px',
border: '1px solid #e9ecef'
}}>
<h3 style={{ margin: '0 0 10px 0', color: '#4a90e2' }}>What's Happening Here?</h3>
<p style={{ margin: '0', lineHeight: '1.5' }}>
This is App1 running on port 3001. Notice in the Runtime Information that App2's module is using <code>app1_partial.js</code> instead
of <code>remoteEntry.js</code>. 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.
</p>
</div>

<div style={{ marginBottom: '20px' }}>
<h3>Shared State Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button>
</div>

<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div>
<h3>Local Button:</h3>
<LocalButton />
</div>
<div>
<h3>Remote Button (from App 2):</h3>
<React.Suspense fallback="Loading Remote Button...">
<RemoteButton />
</React.Suspense>
</div>
</div>

return color;
<div style={{ marginTop: '20px' }}>
<h3>Runtime Information:</h3>
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}>
{__FEDERATION__.__INSTANCES__.map(instance => (
<div key={instance.name} style={{ margin: '10px 0' }}>
<div>
<strong>Module: </strong>{instance.name}
</div>
{instance.options?.remotes?.length > 0 && (
<div style={{ marginLeft: '20px', fontSize: '14px' }}>
<strong>Remote Entries:</strong>
{instance.options.remotes.map((remote, idx) => (
<div key={idx} style={{ marginTop: '5px', color: '#666' }}>
{remote.alias || remote.name}: <code>{remote.entry}</code>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
);
};

// The main App component
const App = () => (
<div>
<h1>Single Runtime</h1>
<h2>Remotes currently in use</h2>
{/* Display the names of the remotes loaded by the CustomPlugin */}
{__FEDERATION__.__INSTANCES__.map(inst => (
<span
style={{
padding: 10,
color: '#fff',
background: getColorFromString(inst.name.split().reverse().join('')),
}}
key={inst.name}
>
{inst.name}
</span>
))}
<p>
Click The second button. This will cause the <i>pick-remote.ts</i> to load remoteEntry urls
from a mock api call.
</p>
{/* LocalButton is a button component from the local app */}
<LocalButton />
{/* RemoteButton is a button component loaded from a remote app */}
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
{/* The Reset button clears the 'button' item from localStorage */}
<button
onClick={() => {
localStorage.clear('button');
window.location.reload();
}}
>
Reset{' '}
</button>
</div>
);

export default App;
26 changes: 20 additions & 6 deletions runtime-plugins/single-runtime/app1/src/Button.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import React from 'react';

const style = {
background: '#800',
color: '#fff',
padding: 12,
};
const Button = () => {
const [clickCount, setClickCount] = React.useState(0);

const style = {
background: '#4a90e2',
color: '#fff',
padding: '10px 20px',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
};

const Button = () => <button style={style}>App 1 Button</button>;
return (
<button
style={style}
onClick={() => setClickCount(c => c + 1)}
>
App 1 Button (Clicks: {clickCount})
</button>
);
};

export default Button;
1 change: 1 addition & 0 deletions runtime-plugins/single-runtime/app2/single-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export default function () {
return {
name: 'single-runtime-plugin',
init(args) {
if(!__FEDERATION__.__INSTANCES__[0]) return args;
const hostName = __FEDERATION__.__INSTANCES__[0].name;
if (args.options.name !== hostName) {
const hostsRemote = args.options.remotes.find(remote => {
Expand Down
92 changes: 80 additions & 12 deletions runtime-plugins/single-runtime/app2/src/App.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,85 @@
import LocalButton from './Button';
import React from 'react';
import LocalButton from './Button';
import RemoteButton from 'app1/Button';

const App = () => {
const [count, setCount] = React.useState(0);

return (
<div style={{ fontFamily: 'Arial, sans-serif', padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<div style={{ marginBottom: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h1>App 2 - Single Runtime Demo</h1>
<a
href="http://localhost:3001"
style={{
padding: '8px 16px',
background: '#4a90e2',
color: 'white',
textDecoration: 'none',
borderRadius: '4px',
fontSize: '14px'
}}
>
← Go to App 1
</a>
</div>

const RemoteButton = React.lazy(() => import('app1/Button'));
<div style={{
padding: '15px',
background: '#f8f9fa',
borderRadius: '4px',
marginBottom: '20px',
border: '1px solid #e9ecef'
}}>
<h3 style={{ margin: '0 0 10px 0', color: '#e24a90' }}>What's Happening Here?</h3>
<p style={{ margin: '0', lineHeight: '1.5' }}>
This is App2 running on port 3002. When loading App1's remote components, it uses the standard <code>remoteEntry.js</code> because
App1 is not the host in this context. Since there's no host/remote pattern here, App1 needs its full standalone runtime to operate.
</p>
</div>

<div style={{ marginBottom: '20px' }}>
<h3>Shared State Counter: {count}</h3>
<button onClick={() => setCount(c => c + 1)}>Increment Counter</button>
</div>

<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<div>
<h3>Local Button:</h3>
<LocalButton />
</div>
<div>
<h3>Remote Button (from App 1):</h3>
<React.Suspense fallback="Loading Remote Button...">
<RemoteButton />
</React.Suspense>
</div>
</div>

const App = () => (
<div>
<h1>API controlled remote configs</h1>
<h2>App 2</h2>
<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
</div>
);
<div style={{ marginTop: '20px' }}>
<h3>Runtime Information:</h3>
<div style={{ background: '#f0f0f0', padding: '10px', borderRadius: '4px' }}>
{__FEDERATION__.__INSTANCES__.map(instance => (
<div key={instance.name} style={{ margin: '10px 0' }}>
<div>
<strong>Module: </strong>{instance.name}
</div>
{instance.options?.remotes?.length > 0 && (
<div style={{ marginLeft: '20px', fontSize: '14px' }}>
<strong>Remote Entries:</strong>
{instance.options.remotes.map((remote, idx) => (
<div key={idx} style={{ marginTop: '5px', color: '#666' }}>
{remote.alias || remote.name}: <code>{remote.entry}</code>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
);
};

export default App;
Loading

0 comments on commit 9ef3c07

Please sign in to comment.