diff --git a/packages/examples/react-vite-dynamic/README.md b/packages/examples/react-vite-dynamic/README.md new file mode 100644 index 00000000..ef5e2f34 --- /dev/null +++ b/packages/examples/react-vite-dynamic/README.md @@ -0,0 +1,14 @@ +# React - Vite Federation Demo + +This example demos consumption of federated modules from a vite bundle. `host` (react based) depends on a component exposed by `remote` app (react based). + +## Running + +Install `pnpm` as per instructions provided [here](https://pnpm.io/installation) + +Run `pnpm install`, then `pnpm run build` and `pnpm run serve`. This will build and serve both `host` and `remote` on ports 5000, 5001 respectively. + +- HOST: [localhost:5000](http://localhost:5000/) +- REMOTE: [localhost:5001](http://localhost:5001/) + +`CTRL + C` can only stop the host server. You can run `pnpm stop` to stop all services. diff --git a/packages/examples/react-vite-dynamic/__tests__/react-vite.spec.ts b/packages/examples/react-vite-dynamic/__tests__/react-vite.spec.ts new file mode 100644 index 00000000..d5b3e742 --- /dev/null +++ b/packages/examples/react-vite-dynamic/__tests__/react-vite.spec.ts @@ -0,0 +1,22 @@ +import { browserLogs, page } from '~utils' +import { expect, test } from 'vitest' + +test('should have no 404s', () => { + browserLogs.forEach((msg) => { + expect(msg).not.toMatch('404') + }) +}) + +test('remote button', async () => { + expect( + await page.textContent('#click-btn') + ).toBe('Click me: 0') +}) + +test('click event', async () => { + await page.click('#click-btn'); + expect( + await page.textContent('#click-btn') + ).toBe('Click me: 1') +}) + diff --git a/packages/examples/react-vite-dynamic/host/index.html b/packages/examples/react-vite-dynamic/host/index.html new file mode 100644 index 00000000..79c47019 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/packages/examples/react-vite-dynamic/host/package.json b/packages/examples/react-vite-dynamic/host/package.json new file mode 100644 index 00000000..a334cbce --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/package.json @@ -0,0 +1,23 @@ +{ + "name": "@react-vite-dynamic/app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 5000 --strictPort", + "build": "vite build", + "preview": "vite preview --port 5000 --strictPort", + "serve": "vite preview --port 5000 --strictPort" + }, + "dependencies": { + "@originjs/vite-plugin-federation": "^1.2.3", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^3.0.0", + "vite": "^4.3.2" + } +} diff --git a/packages/examples/react-vite-dynamic/host/public/vite.svg b/packages/examples/react-vite-dynamic/host/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/host/src/App.css b/packages/examples/react-vite-dynamic/host/src/App.css new file mode 100644 index 00000000..2c5e2ef5 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/App.css @@ -0,0 +1,41 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/packages/examples/react-vite-dynamic/host/src/App.jsx b/packages/examples/react-vite-dynamic/host/src/App.jsx new file mode 100644 index 00000000..e5ffaeec --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/App.jsx @@ -0,0 +1,30 @@ +import reactLogo from './assets/react.svg' +import './App.css' +import {RemoteButton} from "./RemoteComponent.jsx"; + +function App() { + return ( +
+
+ + Vite logo + + + React logo + +
+

Vite + React

+
+ +

+ Edit src/App.jsx and save to test HMR +

+
+

+ Click on the Vite and React logos to learn more +

+
+ ) +} + +export default App diff --git a/packages/examples/react-vite-dynamic/host/src/RemoteComponent.jsx b/packages/examples/react-vite-dynamic/host/src/RemoteComponent.jsx new file mode 100644 index 00000000..581c87d2 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/RemoteComponent.jsx @@ -0,0 +1,74 @@ +import {lazy, Suspense} from "react"; +import {ErrorBoundary} from "react-error-boundary"; + +const remotesMap = {}; +// const __vite__import = name => import(/* @vite-ignore */name); + +const shareScope = { + // 'react': { + // 'default': { + // get: () => __vite__import('http://localhost:5001/assets/__federation_shared_react.js'), + // loaded: 1 + // } + // }, + // 'react-dom': { + // 'default': { + // get: () => __vite__import('http://localhost:5001/assets/__federation_shared_react-dom.js'), + // loaded: 1 + // } + // }, +}; + +if (!globalThis.__federation_shared__){ + globalThis.__federation_shared__=shareScope +} +var __federation__ = { + ensure: async (remoteId) => { + const remote = remotesMap[remoteId]; + if (remote.inited) { + return remote.lib; + } + return new Promise((resolve) => { + // debugger; + import(/* @vite-ignore */ remote.url).then((lib) => { + debugger; + console.log('lib', lib); + if (!remote.inited) { + lib.init(globalThis.__federation_shared__); + remote.lib = lib; + remote.inited = true; + } + resolve(remote.lib); + }); + }); + }, +}; +const loadComponent = (url, scope, module) => async () => { + remotesMap[scope] = { + url: url, + }; + + return __federation__.ensure(scope).then((remote) => remote.get(module).then((factory) => factory())); +}; + +const ModuleLoader = ({module, props, scope, url}) => { + if (!module) { + return Module name cannot be empty; + } + + const Component = lazy(loadComponent(url, scope, module)); + + return ( + Loading...}> + + + ); +}; + +export const RemoteButton = () => { + const url = 'http://localhost:5001/assets/remoteEntry.js' + + return Failed to load remote component...}> + + +} diff --git a/packages/examples/react-vite-dynamic/host/src/assets/react.svg b/packages/examples/react-vite-dynamic/host/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/host/src/index.css b/packages/examples/react-vite-dynamic/host/src/index.css new file mode 100644 index 00000000..917888c1 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/index.css @@ -0,0 +1,70 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/packages/examples/react-vite-dynamic/host/src/main.jsx b/packages/examples/react-vite-dynamic/host/src/main.jsx new file mode 100644 index 00000000..9af0bb63 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) diff --git a/packages/examples/react-vite-dynamic/host/vite.config.js b/packages/examples/react-vite-dynamic/host/vite.config.js new file mode 100644 index 00000000..e0fc7580 --- /dev/null +++ b/packages/examples/react-vite-dynamic/host/vite.config.js @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' +import federation from '@originjs/vite-plugin-federation' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + federation({ + name: 'app', + remotes: {}, + shared: ['react','react-dom'] + }) + ], + build: { + modulePreload: false, + target: 'esnext', + minify: false, + cssCodeSplit: false + } +}) diff --git a/packages/examples/react-vite-dynamic/package.json b/packages/examples/react-vite-dynamic/package.json new file mode 100644 index 00000000..d3282093 --- /dev/null +++ b/packages/examples/react-vite-dynamic/package.json @@ -0,0 +1,20 @@ +{ + "name": "react-vite-dynamic", + "private": true, + "version": "1.0.0", + "scripts": { + "build": "pnpm --parallel --filter \"./**\" build", + "serve": "pnpm --parallel --filter \"./**\" preview", + "build:remotes": "pnpm --parallel --filter \"./remote\" build", + "serve:remotes": "pnpm --parallel --filter \"./remote\" serve", + "dev:hosts": "pnpm --filter \"./host\" dev", + "stop": "kill-port --port 5000,5001" + }, + "devDependencies": { + "kill-port": "^2.0.1" + }, + "dependencies": { + "@originjs/vite-plugin-federation": "^1.2.3", + "react-error-boundary": "^4.0.10" + } +} diff --git a/packages/examples/react-vite-dynamic/pnpm-workspace.yaml b/packages/examples/react-vite-dynamic/pnpm-workspace.yaml new file mode 100644 index 00000000..0a8dec21 --- /dev/null +++ b/packages/examples/react-vite-dynamic/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "host" + - "remote" diff --git a/packages/examples/react-vite-dynamic/remote/index.html b/packages/examples/react-vite-dynamic/remote/index.html new file mode 100644 index 00000000..79c47019 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/packages/examples/react-vite-dynamic/remote/package.json b/packages/examples/react-vite-dynamic/remote/package.json new file mode 100644 index 00000000..6d6f96fb --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/package.json @@ -0,0 +1,22 @@ +{ + "name": "@react-vite-dynamic/shared", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 5001 --strictPort", + "build": "vite build", + "preview": "vite preview --port 5001 --strictPort", + "serve": "vite preview --port 5001 --strictPort" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.17", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^3.0.0", + "vite": "^4.3.2" + } +} \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/remote/public/vite.svg b/packages/examples/react-vite-dynamic/remote/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/remote/src/App.css b/packages/examples/react-vite-dynamic/remote/src/App.css new file mode 100644 index 00000000..2c5e2ef5 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/App.css @@ -0,0 +1,41 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/packages/examples/react-vite-dynamic/remote/src/App.jsx b/packages/examples/react-vite-dynamic/remote/src/App.jsx new file mode 100644 index 00000000..475d22e3 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/App.jsx @@ -0,0 +1,33 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import './App.css' +import { Button } from "./components/Button" + +function App() { + const [count, setCount] = useState(0) + + return ( +
+
+ + Vite logo + + + React logo + +
+

Vite + React

+
+
+

+ Click on the Vite and React logos to learn more +

+
+ ) +} + +export default App diff --git a/packages/examples/react-vite-dynamic/remote/src/assets/react.svg b/packages/examples/react-vite-dynamic/remote/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/remote/src/components/Button.css b/packages/examples/react-vite-dynamic/remote/src/components/Button.css new file mode 100644 index 00000000..7be6694f --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/components/Button.css @@ -0,0 +1,9 @@ +.shared-btn { + background-color: skyblue; + border: 1px solid white; + color: white; + padding: 15px 30px; + text-align: center; + text-decoration: none; + font-size: 18px; +} \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/remote/src/components/Button.jsx b/packages/examples/react-vite-dynamic/remote/src/components/Button.jsx new file mode 100644 index 00000000..5fa941bd --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/components/Button.jsx @@ -0,0 +1,15 @@ + +import "./Button.css" + +import { useState } from "react" + +export const Button = () => { + const [state, setState] = useState(0) + return ( +
+ +
+ ) +} + +export default Button \ No newline at end of file diff --git a/packages/examples/react-vite-dynamic/remote/src/index.css b/packages/examples/react-vite-dynamic/remote/src/index.css new file mode 100644 index 00000000..917888c1 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/index.css @@ -0,0 +1,70 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/packages/examples/react-vite-dynamic/remote/src/main.jsx b/packages/examples/react-vite-dynamic/remote/src/main.jsx new file mode 100644 index 00000000..9af0bb63 --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/src/main.jsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +) diff --git a/packages/examples/react-vite-dynamic/remote/vite.config.js b/packages/examples/react-vite-dynamic/remote/vite.config.js new file mode 100644 index 00000000..2d4d045f --- /dev/null +++ b/packages/examples/react-vite-dynamic/remote/vite.config.js @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import federation from '@originjs/vite-plugin-federation' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + federation({ + name: "remote_app", + filename: "remoteEntry.js", + exposes: { + './Button': './src/components/Button' + }, + shared: ['react','react-dom'] + }) + ], + build: { + modulePreload: false, + target: 'esnext', + minify: false, + cssCodeSplit: false + } +})