Skip to content

feat: single runtime example #4317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 4, 2024
Merged
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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,14 @@
"forever": "4.0.3",
"husky": "9.0.11",
"jest": "29.7.0",
"js-yaml": "4.1.0",
"lerna": "8.1.8",
"lint-staged": "^15.2.10",
"mocha": "10.6.0",
"prettier": "3.3.3",
"pretty-quick": "4.0.0",
"typescript": "5.5.3",
"js-yaml": "4.1.0",
"semver": "7.6.3"
"semver": "7.6.3",
"typescript": "5.5.3"
},
"scripts": {
"list:all": "pnpm list --filter \"*\" --only-projects --depth -1 --json",
Expand Down
1,705 changes: 1,310 additions & 395 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

61 changes: 45 additions & 16 deletions runtime-plugins/control-sharing/README.md
Original file line number Diff line number Diff line change
@@ -1,35 +1,64 @@
# Controlled Vendor Sharing

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.
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.

## Features

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

## Main Components

### `./app1/control-share.ts`
### `control-share.ts`

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

### `./app1/src/ControlPanel.js`
### E2E Tests (`checkAutomaticVendorApps.cy.ts`)

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

# Running Demo
## Running Demo

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

- [localhost:3001](http://localhost:3001/)
- [localhost:3002](http://localhost:3002/)
- [localhost:3001](http://localhost:3001/) - Host application with control panel
- [localhost:3002](http://localhost:3002/) - Remote application

# Running Cypress E2E Tests
## Running Cypress E2E Tests

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)
To run tests in interactive mode:
```bash
npm run cypress:debug
```

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.
To run tests in headless mode:
```bash
yarn e2e:ci
```

["Best Practices, Rules amd more interesting information here](../../cypress/README.md)
For failed tests, screenshots and videos will be saved in the `cypress` directory.

## Implementation Details

The control panel allows you to:
- View current versions of shared modules (react, react-dom, lodash)
- Override versions for specific applications
- Save settings to localStorage
- Clear settings and reload to default versions

The runtime plugin (`control-share.ts`) handles:
- Version resolution based on localStorage settings
- Share scope management
- Instance tracking and updates
- Cross-application module sharing rules
146 changes: 119 additions & 27 deletions runtime-plugins/control-sharing/app1/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,129 @@ import ReactDOM from 'react-dom';
import RemoteButton from 'app2/Button';
import lodash from 'lodash';
import ControlPanel from './ControlPanel';

const styles = {
container: {
padding: '2rem',
maxWidth: '1200px',
margin: '0 auto',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
},
header: {
borderBottom: '2px solid #e9ecef',
marginBottom: '2rem',
paddingBottom: '1rem',
},
title: {
fontSize: '2.5rem',
color: '#2c3e50',
margin: '0 0 1rem 0',
},
subtitle: {
fontSize: '1.8rem',
color: '#34495e',
margin: '1rem 0',
},
versionInfo: {
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
backgroundColor: '#f8f9fa',
padding: '1.5rem',
borderRadius: '8px',
marginBottom: '2rem',
boxShadow: '0 2px 4px rgba(0,0,0,0.05)',
},
versionText: {
margin: '0',
fontSize: '1rem',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
},
dot: {
width: '8px',
height: '8px',
borderRadius: '50%',
display: 'inline-block',
marginRight: '8px',
},
buttonContainer: {
display: 'flex',
gap: '1rem',
marginBottom: '2rem',
},
};

const getColorFromString = str => {
let primes = [1, 2, 3, 5, 7, 11, 13, 17, 19, 23];
const colors = [
'#F44336', // red
'#2196F3', // blue
'#4CAF50', // green
'#9C27B0', // purple
'#E91E63', // pink
'#FF9800', // orange
'#03A9F4', // light blue
'#009688', // teal
'#8BC34A', // light green
'#AB47BC' // medium purple
];

let hash = 0;
for (let i = 0; i < str.length; i++) {
hash += str.charCodeAt(i) * primes[i % primes.length];
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ('00' + value.toString(16)).substr(-2);
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash; // Convert to 32-bit integer
}
return color;

hash = Math.abs(hash);

return colors[hash % colors.length];
};

const App = () => {
const reactColor = getColorFromString(React.version);
const reactDomColor = getColorFromString(ReactDOM.version);
const lodashColor = getColorFromString(lodash.VERSION);

return (
<div style={styles.container}>
<header style={styles.header}>
<h1 style={styles.title}>Share Control Panel</h1>
<h2 style={styles.subtitle}>App 1</h2>
</header>

<div style={styles.versionInfo}>
<h4 style={{ ...styles.versionText, color: reactColor }}>
Host Used React: {React.version}
</h4>
<h4 style={{ ...styles.versionText, color: reactDomColor }}>
Host Used ReactDOM: {ReactDOM.version}
</h4>
<h4 style={{ ...styles.versionText, color: lodashColor }}>
Host Used Lodash: {lodash.VERSION}
</h4>
</div>

<div style={styles.buttonContainer}>
<LocalButton />
<React.Suspense fallback={
<div style={{
padding: '0.5rem 1rem',
backgroundColor: '#f8f9fa',
borderRadius: '4px',
color: '#666'
}}>
Loading Button...
</div>
}>
<RemoteButton />
</React.Suspense>
</div>

<ControlPanel />
</div>
);
};
const App = () => (
<div>
<h1>Share Control Panel</h1>
<h2>App 1</h2>
<h4 style={{ color: getColorFromString(React.version) }}>Host Used React: {React.version}</h4>
<h4 style={{ color: getColorFromString(ReactDOM.version) }}>
Host Used ReactDOM: {ReactDOM.version}
</h4>
<h4 style={{ color: getColorFromString(lodash.VERSION) }}>
Host Used Lodash: {lodash.VERSION}
</h4>

<LocalButton />
<React.Suspense fallback="Loading Button">
<RemoteButton />
</React.Suspense>
<ControlPanel />
</div>
);

export default App;
29 changes: 26 additions & 3 deletions runtime-plugins/control-sharing/app1/src/Button.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import React from 'react';

const style = {
background: '#800',
background: '#ff4444',
color: '#fff',
padding: 12,
padding: '12px 24px',
border: 'none',
borderRadius: '6px',
fontSize: '16px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
};

const Button = () => <button style={style}>App 1 Button</button>;
const Button = () => (
<button
style={style}
onMouseEnter={(e) => {
e.target.style.transform = 'translateY(-2px)';
e.target.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
e.target.style.background = '#ff5555';
}}
onMouseLeave={(e) => {
e.target.style.transform = 'translateY(0)';
e.target.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)';
e.target.style.background = '#ff4444';
}}
>
App 1 Button
</button>
);

export default Button;
Loading
Loading