Skip to content

Commit 834122a

Browse files
feat: single runtime example (#4317)
* feat: single runtime example * upd * feat: single runtime example * feat: single runtime example * fix: add e2e tests * lint-staged * fix: add e2e tests * cix: update app and e2e * cix: update app and e2e * fix: update app and e2e * fix: update app and e2e
1 parent d288f00 commit 834122a

File tree

21 files changed

+2497
-910
lines changed

21 files changed

+2497
-910
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,13 +195,14 @@
195195
"forever": "4.0.3",
196196
"husky": "9.0.11",
197197
"jest": "29.7.0",
198+
"js-yaml": "4.1.0",
198199
"lerna": "8.1.8",
200+
"lint-staged": "^15.2.10",
199201
"mocha": "10.6.0",
200202
"prettier": "3.3.3",
201203
"pretty-quick": "4.0.0",
202-
"typescript": "5.5.3",
203-
"js-yaml": "4.1.0",
204-
"semver": "7.6.3"
204+
"semver": "7.6.3",
205+
"typescript": "5.5.3"
205206
},
206207
"scripts": {
207208
"list:all": "pnpm list --filter \"*\" --only-projects --depth -1 --json",

pnpm-lock.yaml

Lines changed: 1310 additions & 395 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,64 @@
11
# Controlled Vendor Sharing
22

3-
Dynamic Vendor Sharing is an application that implements a control panel in the runtime plugin for module federation 1.5 in rspack or `@module-federation/enhanced`. The control panel allows you to deterministically manage and modify the rules for shared modules, as well as upgrade or downgrade applications based on the inputs from the React form.
3+
This example demonstrates a runtime plugin implementation for Module Federation that provides dynamic control over shared module versions. It allows you to deterministically manage and modify shared module versions across federated applications using a control panel interface and localStorage persistence.
44

55
## Features
66

7-
- Runtime plugin that implements rules for module sharing.
8-
- React form for modifying the rules.
9-
- Ability to upgrade or downgrade applications.
10-
- `app1` and `app2` exposing different button components.
7+
- Runtime plugin for dynamic version control of shared modules
8+
- Control panel UI for managing shared module versions
9+
- Persistent version settings using localStorage (`formDataVMSC` key)
10+
- Support for upgrading/downgrading shared module versions
11+
- E2E tests to verify version control functionality
1112

1213
## Main Components
1314

14-
### `./app1/control-share.ts`
15+
### `control-share.ts`
1516

16-
This is the runtime plugin that implements the rules for module federation.
17+
A runtime plugin that implements version control for Module Federation. Key features:
18+
- Implements the `FederationRuntimePlugin` interface
19+
- Uses localStorage to persist version preferences
20+
- Handles version resolution and module sharing between applications
21+
- Manages share scope mapping and instance tracking
1722

18-
### `./app1/src/ControlPanel.js`
23+
### E2E Tests (`checkAutomaticVendorApps.cy.ts`)
1924

20-
This is a React form that allows for the modification of rules implemented in `control-share.ts`.
25+
Comprehensive E2E tests that verify:
26+
- Initial shared module versions
27+
- Version override functionality through localStorage
28+
- UI updates reflecting version changes
29+
- Button component rendering with correct version information
2130

22-
# Running Demo
31+
## Running Demo
2332

2433
Run `pnpm run start`. This will build and serve both `app1` and `app2` on ports 3001 and 3002 respectively.
2534

26-
- [localhost:3001](http://localhost:3001/)
27-
- [localhost:3002](http://localhost:3002/)
35+
- [localhost:3001](http://localhost:3001/) - Host application with control panel
36+
- [localhost:3002](http://localhost:3002/) - Remote application
2837

29-
# Running Cypress E2E Tests
38+
## Running Cypress E2E Tests
3039

31-
To run tests in interactive mode, run `npm run cypress:debug` from the root directory of the project. It will open Cypress Test Runner and allow to run tests in interactive mode. [More info about "How to run tests"](../../cypress/README.md#how-to-run-tests)
40+
To run tests in interactive mode:
41+
```bash
42+
npm run cypress:debug
43+
```
3244

33-
To build app and run test in headless mode, run `yarn e2e:ci`. It will build app and run tests for this workspace in headless mode. If tets failed cypress will create `cypress` directory in sample root folder with screenshots and videos.
45+
To run tests in headless mode:
46+
```bash
47+
yarn e2e:ci
48+
```
3449

35-
["Best Practices, Rules amd more interesting information here](../../cypress/README.md)
50+
For failed tests, screenshots and videos will be saved in the `cypress` directory.
51+
52+
## Implementation Details
53+
54+
The control panel allows you to:
55+
- View current versions of shared modules (react, react-dom, lodash)
56+
- Override versions for specific applications
57+
- Save settings to localStorage
58+
- Clear settings and reload to default versions
59+
60+
The runtime plugin (`control-share.ts`) handles:
61+
- Version resolution based on localStorage settings
62+
- Share scope management
63+
- Instance tracking and updates
64+
- Cross-application module sharing rules

runtime-plugins/control-sharing/app1/src/App.js

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,129 @@ import ReactDOM from 'react-dom';
44
import RemoteButton from 'app2/Button';
55
import lodash from 'lodash';
66
import ControlPanel from './ControlPanel';
7+
8+
const styles = {
9+
container: {
10+
padding: '2rem',
11+
maxWidth: '1200px',
12+
margin: '0 auto',
13+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
14+
},
15+
header: {
16+
borderBottom: '2px solid #e9ecef',
17+
marginBottom: '2rem',
18+
paddingBottom: '1rem',
19+
},
20+
title: {
21+
fontSize: '2.5rem',
22+
color: '#2c3e50',
23+
margin: '0 0 1rem 0',
24+
},
25+
subtitle: {
26+
fontSize: '1.8rem',
27+
color: '#34495e',
28+
margin: '1rem 0',
29+
},
30+
versionInfo: {
31+
display: 'flex',
32+
flexDirection: 'column',
33+
gap: '0.5rem',
34+
backgroundColor: '#f8f9fa',
35+
padding: '1.5rem',
36+
borderRadius: '8px',
37+
marginBottom: '2rem',
38+
boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
39+
},
40+
versionText: {
41+
margin: '0',
42+
fontSize: '1rem',
43+
fontWeight: '500',
44+
display: 'flex',
45+
alignItems: 'center',
46+
gap: '0.5rem',
47+
},
48+
dot: {
49+
width: '8px',
50+
height: '8px',
51+
borderRadius: '50%',
52+
display: 'inline-block',
53+
marginRight: '8px',
54+
},
55+
buttonContainer: {
56+
display: 'flex',
57+
gap: '1rem',
58+
marginBottom: '2rem',
59+
},
60+
};
61+
762
const getColorFromString = str => {
8-
let primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
63+
const colors = [
64+
'#F44336', // red
65+
'#2196F3', // blue
66+
'#4CAF50', // green
67+
'#9C27B0', // purple
68+
'#E91E63', // pink
69+
'#FF9800', // orange
70+
'#03A9F4', // light blue
71+
'#009688', // teal
72+
'#8BC34A', // light green
73+
'#AB47BC' // medium purple
74+
];
75+
976
let hash = 0;
1077
for (let i = 0; i < str.length; i++) {
11-
hash += str.charCodeAt(i) * primes[i % primes.length];
12-
}
13-
let color = '#';
14-
for (let i = 0; i < 3; i++) {
15-
const value = (hash >> (i * 8)) & 0xff;
16-
color += ('00' + value.toString(16)).substr(-2);
78+
hash = ((hash << 5) - hash) + str.charCodeAt(i);
79+
hash = hash & hash; // Convert to 32-bit integer
1780
}
18-
return color;
81+
82+
hash = Math.abs(hash);
83+
84+
return colors[hash % colors.length];
85+
};
86+
87+
const App = () => {
88+
const reactColor = getColorFromString(React.version);
89+
const reactDomColor = getColorFromString(ReactDOM.version);
90+
const lodashColor = getColorFromString(lodash.VERSION);
91+
92+
return (
93+
<div style={styles.container}>
94+
<header style={styles.header}>
95+
<h1 style={styles.title}>Share Control Panel</h1>
96+
<h2 style={styles.subtitle}>App 1</h2>
97+
</header>
98+
99+
<div style={styles.versionInfo}>
100+
<h4 style={{ ...styles.versionText, color: reactColor }}>
101+
Host Used React: {React.version}
102+
</h4>
103+
<h4 style={{ ...styles.versionText, color: reactDomColor }}>
104+
Host Used ReactDOM: {ReactDOM.version}
105+
</h4>
106+
<h4 style={{ ...styles.versionText, color: lodashColor }}>
107+
Host Used Lodash: {lodash.VERSION}
108+
</h4>
109+
</div>
110+
111+
<div style={styles.buttonContainer}>
112+
<LocalButton />
113+
<React.Suspense fallback={
114+
<div style={{
115+
padding: '0.5rem 1rem',
116+
backgroundColor: '#f8f9fa',
117+
borderRadius: '4px',
118+
color: '#666'
119+
}}>
120+
Loading Button...
121+
</div>
122+
}>
123+
<RemoteButton />
124+
</React.Suspense>
125+
</div>
126+
127+
<ControlPanel />
128+
</div>
129+
);
19130
};
20-
const App = () => (
21-
<div>
22-
<h1>Share Control Panel</h1>
23-
<h2>App 1</h2>
24-
<h4 style={{ color: getColorFromString(React.version) }}>Host Used React: {React.version}</h4>
25-
<h4 style={{ color: getColorFromString(ReactDOM.version) }}>
26-
Host Used ReactDOM: {ReactDOM.version}
27-
</h4>
28-
<h4 style={{ color: getColorFromString(lodash.VERSION) }}>
29-
Host Used Lodash: {lodash.VERSION}
30-
</h4>
31-
32-
<LocalButton />
33-
<React.Suspense fallback="Loading Button">
34-
<RemoteButton />
35-
</React.Suspense>
36-
<ControlPanel />
37-
</div>
38-
);
39131

40132
export default App;
Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,34 @@
11
import React from 'react';
22

33
const style = {
4-
background: '#800',
4+
background: '#ff4444',
55
color: '#fff',
6-
padding: 12,
6+
padding: '12px 24px',
7+
border: 'none',
8+
borderRadius: '6px',
9+
fontSize: '16px',
10+
fontWeight: '600',
11+
cursor: 'pointer',
12+
transition: 'all 0.2s ease',
13+
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
714
};
815

9-
const Button = () => <button style={style}>App 1 Button</button>;
16+
const Button = () => (
17+
<button
18+
style={style}
19+
onMouseEnter={(e) => {
20+
e.target.style.transform = 'translateY(-2px)';
21+
e.target.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
22+
e.target.style.background = '#ff5555';
23+
}}
24+
onMouseLeave={(e) => {
25+
e.target.style.transform = 'translateY(0)';
26+
e.target.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
27+
e.target.style.background = '#ff4444';
28+
}}
29+
>
30+
App 1 Button
31+
</button>
32+
);
1033

1134
export default Button;

0 commit comments

Comments
 (0)