Skip to content

Commit

Permalink
feat: single runtime example (#4317)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ScriptedAlchemy authored Dec 4, 2024
1 parent d288f00 commit 834122a
Show file tree
Hide file tree
Showing 21 changed files with 2,497 additions and 910 deletions.
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

0 comments on commit 834122a

Please sign in to comment.