├── CNAME ├── packages ├── mf-green │ ├── src │ │ ├── modules.d.ts │ │ ├── images │ │ │ ├── reco_1.jpg │ │ │ ├── reco_2.jpg │ │ │ ├── reco_3.jpg │ │ │ ├── reco_4.jpg │ │ │ ├── reco_5.jpg │ │ │ ├── reco_6.jpg │ │ │ ├── reco_7.jpg │ │ │ ├── reco_8.jpg │ │ │ └── reco_9.jpg │ │ ├── main.ts │ │ ├── bootstrap.tsx │ │ ├── federation.ts │ │ ├── style │ │ │ └── recommendations.css │ │ └── product-recommendations.tsx │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ └── package.json ├── mf-red │ ├── src │ │ ├── modules.d.ts │ │ ├── images │ │ │ ├── tractor-red.jpg │ │ │ ├── tractor-blue.jpg │ │ │ ├── tractor-green.jpg │ │ │ ├── tractor-blue-thumb.jpg │ │ │ ├── tractor-red-thumb.jpg │ │ │ └── tractor-green-thumb.jpg │ │ ├── bootstrap.tsx │ │ ├── main.ts │ │ ├── federation.ts │ │ ├── style │ │ │ └── product-page.css │ │ └── product-page.tsx │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ └── package.json ├── mf-blue │ ├── src │ │ ├── main.ts │ │ ├── bootstrap.tsx │ │ ├── style │ │ │ ├── basket-info.css │ │ │ └── buy-button.css │ │ ├── federation.ts │ │ ├── buy-button.tsx │ │ └── basket-info.tsx │ ├── tsconfig.json │ ├── index.html │ ├── vite.config.ts │ └── package.json ├── host-indirect │ ├── src │ │ ├── index.ts │ │ ├── bootstrap.tsx │ │ ├── federation.ts │ │ └── style.css │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ └── vite.config.ts ├── host-direct │ ├── src │ │ ├── bootstrap.tsx │ │ ├── index.ts │ │ ├── federation.ts │ │ └── style.css │ ├── tsconfig.json │ ├── index.html │ ├── package.json │ └── vite.config.ts ├── shared │ ├── tsconfig.json │ └── loader.ts └── esbuild │ ├── tsconfig.json │ ├── package.json │ └── src │ ├── collect-exports.ts │ └── index.ts ├── native-feed.png ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── lerna.json ├── package.json ├── LICENSE ├── .gitignore └── README.md /CNAME: -------------------------------------------------------------------------------- 1 | cloud-native-federation.samples.piral.cloud -------------------------------------------------------------------------------- /packages/mf-green/src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | -------------------------------------------------------------------------------- /packages/mf-red/src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | -------------------------------------------------------------------------------- /native-feed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/native-feed.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [smapiot, FlorianRappl] 4 | custom: ['https://www.paypal.me/FlorianRappl'] 5 | -------------------------------------------------------------------------------- /packages/mf-blue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setup } from "@shared/loader"; 2 | 3 | (async () => { 4 | await setup(); 5 | await import("./bootstrap"); 6 | })(); 7 | -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_1.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_2.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_3.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_4.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_5.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_6.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_7.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_8.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/images/reco_9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-green/src/images/reco_9.jpg -------------------------------------------------------------------------------- /packages/mf-green/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setup } from "@shared/loader"; 2 | 3 | (async () => { 4 | await setup(); 5 | await import("./bootstrap"); 6 | })(); 7 | -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-red.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-red.jpg -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/lerna/schemas/lerna-schema.json", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "version": "1.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-blue.jpg -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-green.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-green.jpg -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-blue-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-blue-thumb.jpg -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-red-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-red-thumb.jpg -------------------------------------------------------------------------------- /packages/mf-red/src/images/tractor-green-thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piral-samples/piral-cloud-native-federation-demo/HEAD/packages/mf-red/src/images/tractor-green-thumb.jpg -------------------------------------------------------------------------------- /packages/mf-red/src/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import { renderProductPage } from "./product-page"; 2 | 3 | const app = document.querySelector('#app'); 4 | const page = app.appendChild(document.createElement('div')); 5 | 6 | renderProductPage(page); 7 | -------------------------------------------------------------------------------- /packages/host-indirect/src/index.ts: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | import { setup } from "@shared/loader"; 3 | 4 | (async () => { 5 | await setup('https://native-federation-demo.my.piral.cloud/api/v1/native-federation'); 6 | await import("./bootstrap"); 7 | })(); 8 | -------------------------------------------------------------------------------- /packages/mf-green/src/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import { renderRecommendations } from "./product-recommendations"; 2 | 3 | const app = document.querySelector('#app'); 4 | const recommendations = app.appendChild(document.createElement('div')); 5 | 6 | renderRecommendations(recommendations); 7 | -------------------------------------------------------------------------------- /packages/mf-red/src/main.ts: -------------------------------------------------------------------------------- 1 | import { setup } from "@shared/loader"; 2 | 3 | (async () => { 4 | await setup({ 5 | green: "http://localhost:2003/remoteEntry.json", 6 | blue: "http://localhost:2002/remoteEntry.json", 7 | }); 8 | await import("./bootstrap"); 9 | })(); 10 | -------------------------------------------------------------------------------- /packages/host-direct/src/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import { loadRemoteModule } from "@softarc/native-federation"; 2 | 3 | loadRemoteModule({ 4 | remoteName: "mf-red", 5 | exposedModule: "./productPage", 6 | }).then(({ renderProductPage }) => { 7 | const root = document.querySelector("#app"); 8 | renderProductPage(root); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/host-indirect/src/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import { loadRemoteModule } from "@softarc/native-federation"; 2 | 3 | //TODO 4 | 5 | loadRemoteModule({ 6 | remoteName: "mf-red", 7 | exposedModule: "./productPage", 8 | }).then(({ renderProductPage }) => { 9 | const root = document.querySelector("#app"); 10 | renderProductPage(root); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/host-direct/src/index.ts: -------------------------------------------------------------------------------- 1 | import "./style.css"; 2 | import { setup } from "@shared/loader"; 3 | 4 | (async () => { 5 | await setup({ 6 | "mf-red": "http://localhost:2001/remoteEntry.json", 7 | "mf-blue": "http://localhost:2002/remoteEntry.json", 8 | "mf-green": "http://localhost:2003/remoteEntry.json", 9 | }); 10 | await import("./bootstrap"); 11 | })(); 12 | -------------------------------------------------------------------------------- /packages/mf-blue/src/bootstrap.tsx: -------------------------------------------------------------------------------- 1 | import { renderBasketInfo } from "./basket-info"; 2 | import { renderBuyButton } from "./buy-button"; 3 | 4 | const app = document.querySelector('#app'); 5 | const basketInfo = app.appendChild(document.createElement('div')); 6 | const buyButton = app.appendChild(document.createElement('div')); 7 | 8 | renderBasketInfo(basketInfo); 9 | renderBuyButton(buyButton); 10 | -------------------------------------------------------------------------------- /packages/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true 13 | }, 14 | "include": ["*.ts", "*.tsx"] 15 | } 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "scripts": { 8 | "build": "lerna run build", 9 | "publish:mf": "lerna run publish:mf", 10 | "run:feed": "lerna run preview --stream --scope host-indirect", 11 | "run:local": "lerna run preview --stream --ignore host-indirect" 12 | }, 13 | "devDependencies": { 14 | "lerna": "^6.4.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/esbuild/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "useDefineForClassFields": true, 5 | "target": "es2015", 6 | "module": "commonjs", 7 | "declaration": true, 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "outDir": "dist", 12 | "allowJs": true, 13 | "checkJs": true 14 | }, 15 | "include": ["src/**/*.ts", "src/**/*.tsx"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/host-direct/src/federation.ts: -------------------------------------------------------------------------------- 1 | const { 2 | withNativeFederation, 3 | shareAll, 4 | } = require("@softarc/native-federation/build"); 5 | 6 | module.exports = withNativeFederation({ 7 | name: "host-direct", 8 | filename: "index.js", 9 | exposes: {}, 10 | shared: { 11 | ...shareAll({ 12 | singleton: true, 13 | strictVersion: true, 14 | requiredVersion: "auto", 15 | includeSecondaries: false, 16 | }), 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/host-indirect/src/federation.ts: -------------------------------------------------------------------------------- 1 | const { 2 | withNativeFederation, 3 | shareAll, 4 | } = require("@softarc/native-federation/build"); 5 | 6 | module.exports = withNativeFederation({ 7 | name: "host-indirect", 8 | filename: "index.js", 9 | exposes: {}, 10 | shared: { 11 | ...shareAll({ 12 | singleton: true, 13 | strictVersion: true, 14 | requiredVersion: "auto", 15 | includeSecondaries: false, 16 | }), 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/mf-red/src/federation.ts: -------------------------------------------------------------------------------- 1 | const { 2 | withNativeFederation, 3 | shareAll, 4 | } = require("@softarc/native-federation/build"); 5 | 6 | module.exports = withNativeFederation({ 7 | name: "mf-red", 8 | exposes: { 9 | "./productPage": "./src/product-page.tsx", 10 | }, 11 | shared: { 12 | ...shareAll({ 13 | singleton: true, 14 | strictVersion: true, 15 | requiredVersion: "auto", 16 | includeSecondaries: false, 17 | }), 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/mf-red/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "paths": { 14 | "@shared/*": ["../shared/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/host-direct/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "paths": { 14 | "@shared/*": ["../shared/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/mf-blue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "paths": { 14 | "@shared/*": ["../shared/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/mf-green/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "paths": { 14 | "@shared/*": ["../shared/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/host-indirect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | "jsx": "react", 11 | "allowJs": true, 12 | "checkJs": true, 13 | "paths": { 14 | "@shared/*": ["../shared/*"] 15 | } 16 | }, 17 | "include": ["src/**/*.ts", "src/**/*.tsx"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/mf-green/src/federation.ts: -------------------------------------------------------------------------------- 1 | const { 2 | withNativeFederation, 3 | shareAll, 4 | } = require("@softarc/native-federation/build"); 5 | 6 | module.exports = withNativeFederation({ 7 | name: "mf-green", 8 | exposes: { 9 | "./recommendations": "./src/product-recommendations.tsx", 10 | }, 11 | shared: { 12 | ...shareAll({ 13 | singleton: true, 14 | strictVersion: true, 15 | requiredVersion: "auto", 16 | includeSecondaries: false, 17 | }), 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/mf-blue/src/style/basket-info.css: -------------------------------------------------------------------------------- 1 | #basket { 2 | align-self: baseline; 3 | grid-area: basket; 4 | justify-self: end; 5 | margin-top: 11px; 6 | } 7 | 8 | #basket .empty, 9 | #basket .filled { 10 | border-radius: 5px; 11 | color: white; 12 | padding: 5px 10px; 13 | } 14 | 15 | #basket .empty { 16 | background-color: gray; 17 | } 18 | 19 | #basket .filled { 20 | background-color: seagreen; 21 | } 22 | 23 | .blue-basket { 24 | display: block; 25 | outline: 3px dashed royalblue; 26 | padding: 5px; 27 | } 28 | -------------------------------------------------------------------------------- /packages/mf-blue/src/federation.ts: -------------------------------------------------------------------------------- 1 | const { 2 | withNativeFederation, 3 | shareAll, 4 | } = require("@softarc/native-federation/build"); 5 | 6 | module.exports = withNativeFederation({ 7 | name: "mf-blue", 8 | exposes: { 9 | "./basketInfo": "./src/basket-info.tsx", 10 | "./buyButton": "./src/buy-button.tsx", 11 | }, 12 | remotes: {}, 13 | shared: { 14 | ...shareAll({ 15 | singleton: true, 16 | strictVersion: true, 17 | requiredVersion: "auto", 18 | includeSecondaries: false, 19 | }), 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "native-federation-esbuild", 3 | "version": "1.0.0", 4 | "type": "commonjs", 5 | "scripts": { 6 | "build": "tsc" 7 | }, 8 | "main": "dist/index.js", 9 | "types": "dist/index.d.ts", 10 | "dependencies": { 11 | "@rollup/plugin-commonjs": "^22.0.2", 12 | "@rollup/plugin-node-resolve": "^13.3.0", 13 | "@rollup/plugin-replace": "^4.0.0", 14 | "rollup": "^2.79.0", 15 | "rollup-plugin-node-externals": "^4.1.1", 16 | "esbuild": "0.18.20", 17 | "npmlog": "^6.0.2", 18 | "acorn": "^8.8.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/mf-blue/src/style/buy-button.css: -------------------------------------------------------------------------------- 1 | #buy { 2 | align-self: center; 3 | grid-area: buy; 4 | } 5 | 6 | #buy button { 7 | background: none; 8 | border: 1px solid gray; 9 | border-radius: 20px; 10 | cursor: pointer; 11 | display: block; 12 | font-size: 20px; 13 | outline: none; 14 | padding: 20px; 15 | width: 100%; 16 | } 17 | 18 | #buy button:hover { 19 | border-color: black; 20 | } 21 | 22 | #buy button:active { 23 | border-color: seagreen; 24 | } 25 | 26 | .blue-buy { 27 | display: block; 28 | outline: 3px dashed royalblue; 29 | padding: 5px; 30 | } 31 | -------------------------------------------------------------------------------- /packages/mf-red/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | Red 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/shared/loader.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from "react"; 2 | import { initFederation, loadRemoteModule } from "@softarc/native-federation"; 3 | 4 | export async function setup(manifest?: string | Record) { 5 | await initFederation(manifest); 6 | 7 | window.loadComponent = (remoteName, exposedModule) => 8 | lazy(() => 9 | loadRemoteModule({ 10 | remoteName, 11 | exposedModule, 12 | }) 13 | ); 14 | } 15 | 16 | declare global { 17 | interface Window { 18 | loadComponent(remoteName: string, modulePath: string): React.FC; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/mf-blue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | Blue 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/mf-green/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | 13 | Blue 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/host-direct/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tractor Store (Direct via hardcoded URLs) 6 | 7 | 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/host-indirect/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tractor Store (Indirect via Piral Feed Service) 6 | 7 | 8 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /packages/host-direct/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "host-direct", 3 | "version": "1.0.0", 4 | "description": "App shell using direct / known imports", 5 | "keywords": [], 6 | "author": "Florian Rappl", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "vite --port 1234", 10 | "preview": "npx http-server dist --port 1234 --cors", 11 | "build": "vite build" 12 | }, 13 | "dependencies": { 14 | "@softarc/native-federation": "^2.0.8" 15 | }, 16 | "devDependencies": { 17 | "@module-federation/vite": "^0.2.6", 18 | "@types/node": "18.11.18", 19 | "native-federation-esbuild": "^1.0.0", 20 | "typescript": "^4.9.4", 21 | "vite": "^4.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/host-indirect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "host-indirect", 3 | "version": "1.0.0", 4 | "description": "App shell using imports from a feed", 5 | "keywords": [], 6 | "author": "Florian Rappl", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "vite --port 1234", 10 | "preview": "npx http-server dist --port 1234 --cors", 11 | "build": "vite build" 12 | }, 13 | "dependencies": { 14 | "@softarc/native-federation": "^2.0.8" 15 | }, 16 | "devDependencies": { 17 | "@module-federation/vite": "^0.2.6", 18 | "@types/node": "18.11.18", 19 | "native-federation-esbuild": "^1.0.0", 20 | "typescript": "^4.9.4", 21 | "vite": "^4.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/mf-green/src/style/recommendations.css: -------------------------------------------------------------------------------- 1 | #reco { 2 | grid-area: reco; 3 | } 4 | 5 | @media only screen and (max-width: 999px) { 6 | #reco { 7 | align-items: center; 8 | display: flex; 9 | flex-wrap: wrap; 10 | justify-content: space-around; 11 | margin-top: 20px; 12 | padding-top: 20px; 13 | } 14 | } 15 | 16 | @media only screen and (min-width: 1000px) { 17 | #reco { 18 | justify-content: stretch; 19 | text-align: center; 20 | width: 100%; 21 | } 22 | } 23 | 24 | #reco h3 { 25 | font-weight: 400; 26 | } 27 | 28 | #reco img { 29 | display: inline-block; 30 | height: 180px; 31 | width: 180px; 32 | } 33 | 34 | .green-recos { 35 | display: block; 36 | outline: 3px dashed forestgreen; 37 | width: 100%; 38 | } 39 | -------------------------------------------------------------------------------- /packages/host-direct/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { federation } from "@module-federation/vite"; 3 | import { createEsBuildAdapter } from "native-federation-esbuild"; 4 | 5 | export default defineConfig(async ({ command }) => ({ 6 | resolve: { 7 | alias: { 8 | '@shared/loader': '../shared/loader.ts', 9 | }, 10 | }, 11 | plugins: [ 12 | await federation({ 13 | options: { 14 | workspaceRoot: __dirname, 15 | outputPath: "dist", 16 | tsConfig: "tsconfig.json", 17 | federationConfig: "src/federation.ts", 18 | verbose: false, 19 | dev: command === "serve", 20 | }, 21 | adapter: createEsBuildAdapter({ 22 | plugins: [], 23 | }), 24 | }), 25 | ], 26 | })); 27 | -------------------------------------------------------------------------------- /packages/host-indirect/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { federation } from "@module-federation/vite"; 3 | import { createEsBuildAdapter } from "native-federation-esbuild"; 4 | 5 | export default defineConfig(async ({ command }) => ({ 6 | resolve: { 7 | alias: { 8 | '@shared/loader': '../shared/loader.ts', 9 | }, 10 | }, 11 | plugins: [ 12 | await federation({ 13 | options: { 14 | workspaceRoot: __dirname, 15 | outputPath: "dist", 16 | tsConfig: "tsconfig.json", 17 | federationConfig: "src/federation.ts", 18 | verbose: false, 19 | dev: command === "serve", 20 | }, 21 | adapter: createEsBuildAdapter({ 22 | plugins: [], 23 | }), 24 | }), 25 | ], 26 | })); 27 | -------------------------------------------------------------------------------- /packages/mf-blue/src/buy-button.tsx: -------------------------------------------------------------------------------- 1 | import "./style/buy-button.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | 5 | const defaultPrice = "0,00 €"; 6 | const prices = { 7 | porsche: "66,00 €", 8 | fendt: "54,00 €", 9 | eicher: "58,00 €", 10 | }; 11 | 12 | const BuyButton = ({ sku = "porsche" }) => { 13 | const price = prices[sku] || defaultPrice; 14 | 15 | return ( 16 |
{ 18 | e.preventDefault(); 19 | window.dispatchEvent(new CustomEvent("add-item", { detail: price })); 20 | }} 21 | > 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default BuyButton; 28 | 29 | export function renderBuyButton(container: HTMLElement) { 30 | ReactDOM.render(, container); 31 | } 32 | -------------------------------------------------------------------------------- /packages/mf-blue/src/basket-info.tsx: -------------------------------------------------------------------------------- 1 | import "./style/basket-info.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | 5 | const BasketInfo = ({ sku = "porsche" }) => { 6 | const [items, setItems] = React.useState([]); 7 | const count = items.length; 8 | 9 | React.useEffect(() => { 10 | const handler = () => { 11 | setItems((items) => [...items, sku]); 12 | }; 13 | window.addEventListener("add-item", handler); 14 | return () => window.removeEventListener("add-item", handler); 15 | }, [sku]); 16 | 17 | return ( 18 |
basket: {count} item(s)
19 | ); 20 | }; 21 | 22 | export default BasketInfo; 23 | 24 | export function renderBasketInfo(container: HTMLElement) { 25 | ReactDOM.render(, container); 26 | } 27 | -------------------------------------------------------------------------------- /packages/mf-red/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { style } from "@hyrious/esbuild-plugin-style"; 3 | import { autoPathPlugin } from "esbuild-auto-path-plugin"; 4 | import { federation } from "@module-federation/vite"; 5 | import { createEsBuildAdapter } from "native-federation-esbuild"; 6 | 7 | export default defineConfig(async ({ command }) => ({ 8 | resolve: { 9 | alias: { 10 | '@shared/loader': '../shared/loader.ts', 11 | }, 12 | }, 13 | plugins: [ 14 | await federation({ 15 | options: { 16 | workspaceRoot: __dirname, 17 | outputPath: "dist", 18 | tsConfig: "tsconfig.json", 19 | federationConfig: "src/federation.ts", 20 | verbose: false, 21 | dev: command === "serve", 22 | }, 23 | adapter: createEsBuildAdapter({ 24 | plugins: [autoPathPlugin(), style()], 25 | }), 26 | }), 27 | ], 28 | })); 29 | -------------------------------------------------------------------------------- /packages/mf-blue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { style } from "@hyrious/esbuild-plugin-style"; 3 | import { autoPathPlugin } from "esbuild-auto-path-plugin"; 4 | import { federation } from "@module-federation/vite"; 5 | import { createEsBuildAdapter } from "native-federation-esbuild"; 6 | 7 | export default defineConfig(async ({ command }) => ({ 8 | resolve: { 9 | alias: { 10 | '@shared/loader': '../shared/loader.ts', 11 | }, 12 | }, 13 | plugins: [ 14 | await federation({ 15 | options: { 16 | workspaceRoot: __dirname, 17 | outputPath: "dist", 18 | tsConfig: "tsconfig.json", 19 | federationConfig: "src/federation.ts", 20 | verbose: false, 21 | dev: command === "serve", 22 | }, 23 | adapter: createEsBuildAdapter({ 24 | plugins: [autoPathPlugin(), style()], 25 | }), 26 | }), 27 | ], 28 | })); 29 | -------------------------------------------------------------------------------- /packages/mf-green/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { style } from "@hyrious/esbuild-plugin-style"; 3 | import { autoPathPlugin } from "esbuild-auto-path-plugin"; 4 | import { federation } from "@module-federation/vite"; 5 | import { createEsBuildAdapter } from "native-federation-esbuild"; 6 | 7 | export default defineConfig(async ({ command }) => ({ 8 | resolve: { 9 | alias: { 10 | '@shared/loader': '../shared/loader.ts', 11 | }, 12 | }, 13 | plugins: [ 14 | await federation({ 15 | options: { 16 | workspaceRoot: __dirname, 17 | outputPath: "dist", 18 | tsConfig: "tsconfig.json", 19 | federationConfig: "src/federation.ts", 20 | verbose: false, 21 | dev: command === "serve", 22 | }, 23 | adapter: createEsBuildAdapter({ 24 | plugins: [autoPathPlugin(), style()], 25 | }), 26 | }), 27 | ], 28 | })); 29 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Publish Demo 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: "20.x" 17 | registry-url: "https://registry.npmjs.org" 18 | - name: Install Yarn and gh-pages 19 | run: | 20 | yarn install 21 | npm install -g gh-pages@3.0.0 22 | - name: Build Website 23 | run: | 24 | npx lerna run build 25 | echo "cloud-native-federation.samples.piral.cloud" > packages/host-indirect/dist/CNAME 26 | - name: Deploy Website 27 | run: | 28 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git 29 | gh-pages -d "packages/host-indirect/dist" -u "github-actions-bot " 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /packages/mf-red/src/style/product-page.css: -------------------------------------------------------------------------------- 1 | #store { 2 | font-weight: 400; 3 | grid-area: store; 4 | margin-top: 5px; 5 | } 6 | 7 | #image { 8 | grid-area: image; 9 | width: 100%; 10 | } 11 | 12 | #image > div { 13 | padding-top: 100%; 14 | position: relative; 15 | } 16 | 17 | #image img { 18 | bottom: 0; 19 | left: 0; 20 | max-width: 100%; 21 | position: absolute; 22 | right: 0; 23 | top: 0; 24 | } 25 | 26 | #name { 27 | font-weight: 400; 28 | grid-area: name; 29 | height: 3em; 30 | } 31 | 32 | #name small { 33 | font-size: 1em; 34 | font-weight: 200; 35 | } 36 | 37 | #options { 38 | align-self: center; 39 | display: flex; 40 | grid-area: options; 41 | } 42 | 43 | #options button { 44 | border: none; 45 | border-bottom: 2px solid white; 46 | cursor: pointer; 47 | display: block; 48 | margin: 2px; 49 | outline: none; 50 | padding: 0; 51 | } 52 | 53 | #options button.active, 54 | #options button:hover { 55 | border-bottom-color: seagreen; 56 | } 57 | 58 | #options img { 59 | display: block; 60 | max-width: 100%; 61 | } 62 | -------------------------------------------------------------------------------- /packages/mf-red/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mf-red", 3 | "version": "1.0.3", 4 | "description": "Micro frontend of the red team", 5 | "keywords": [], 6 | "author": "Florian Rappl", 7 | "license": "MIT", 8 | "main": "dist/remoteEntry.json", 9 | "scripts": { 10 | "start": "vite --port 2001", 11 | "preview": "npx http-server dist --port 2001 --cors", 12 | "publish:mf": "npx publish-microfrontend --url https://native-federation-demo.my.piral.cloud/api/v1/pilet --interactive", 13 | "build": "vite build" 14 | }, 15 | "dependencies": { 16 | "@softarc/native-federation": "^2.0.8", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@hyrious/esbuild-plugin-style": "^0.3.5", 22 | "@module-federation/vite": "^0.2.6", 23 | "@types/node": "18.11.18", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "esbuild-auto-path-plugin": "0.15.1", 27 | "native-federation-esbuild": "^1.0.0", 28 | "publish-microfrontend": "1.5.0", 29 | "typescript": "^4.9.4", 30 | "vite": "^4.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/mf-blue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mf-blue", 3 | "version": "1.0.0", 4 | "description": "Micro frontend of the blue team", 5 | "keywords": [], 6 | "author": "Florian Rappl", 7 | "license": "MIT", 8 | "main": "dist/remoteEntry.json", 9 | "scripts": { 10 | "start": "vite --port 2002", 11 | "preview": "npx http-server dist --port 2002 --cors", 12 | "publish:mf": "npx publish-microfrontend --url https://native-federation-demo.my.piral.cloud/api/v1/pilet --interactive", 13 | "build": "vite build" 14 | }, 15 | "dependencies": { 16 | "@softarc/native-federation": "^2.0.8", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@hyrious/esbuild-plugin-style": "^0.3.5", 22 | "@module-federation/vite": "^0.2.6", 23 | "@types/node": "18.11.18", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "esbuild-auto-path-plugin": "0.15.1", 27 | "native-federation-esbuild": "^1.0.0", 28 | "publish-microfrontend": "1.5.0", 29 | "typescript": "^4.9.4", 30 | "vite": "^4.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/mf-green/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mf-green", 3 | "version": "1.0.0", 4 | "description": "Micro frontend of the green team", 5 | "keywords": [], 6 | "author": "Florian Rappl", 7 | "license": "MIT", 8 | "main": "dist/remoteEntry.json", 9 | "scripts": { 10 | "start": "vite --port 2003", 11 | "preview": "npx http-server dist --port 2003 --cors", 12 | "publish:mf": "npx publish-microfrontend --url https://native-federation-demo.my.piral.cloud/api/v1/pilet --interactive", 13 | "build": "vite build" 14 | }, 15 | "dependencies": { 16 | "@softarc/native-federation": "^2.0.8", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0" 19 | }, 20 | "devDependencies": { 21 | "@hyrious/esbuild-plugin-style": "^0.3.5", 22 | "@module-federation/vite": "^0.2.6", 23 | "@types/node": "18.11.18", 24 | "@types/react": "18.0.27", 25 | "@types/react-dom": "18.0.10", 26 | "esbuild-auto-path-plugin": "0.15.1", 27 | "native-federation-esbuild": "^1.0.0", 28 | "publish-microfrontend": "1.5.0", 29 | "typescript": "^4.9.4", 30 | "vite": "^4.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 piral-samples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/host-direct/src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: "Helvetica Neue", Arial, sans-serif; 4 | } 5 | 6 | aside { 7 | border-top: 1px solid gray; 8 | padding-top: 20px; 9 | } 10 | 11 | dl { 12 | display: grid; 13 | grid-column-gap: 15px; 14 | grid-row-gap: 2px; 15 | grid-template-columns: 2fr 1fr; 16 | max-width: 300px; 17 | } 18 | 19 | dt { 20 | text-align: right; 21 | } 22 | 23 | dd { 24 | margin: 0; 25 | } 26 | 27 | #app { 28 | display: grid; 29 | grid-column-gap: 20px; 30 | grid-gap: 20px; 31 | grid-row-gap: 10px; 32 | margin: 20px auto; 33 | min-width: 500px; 34 | } 35 | 36 | #app { 37 | outline: 3px dashed orangered; 38 | padding: 15px; 39 | } 40 | 41 | @media only screen and (max-width: 999px) { 42 | #app { 43 | grid-template-areas: 44 | "store basket" 45 | "image name" 46 | "image options" 47 | "image buy" 48 | "reco reco"; 49 | grid-template-columns: 4fr 3fr; 50 | } 51 | } 52 | 53 | @media only screen and (min-width: 1000px) { 54 | #app { 55 | grid-template-areas: 56 | "store basket reco" 57 | "image name reco" 58 | "image options reco" 59 | "image buy reco"; 60 | grid-template-columns: 4fr 3fr 200px; 61 | width: 1000px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/host-indirect/src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: "Helvetica Neue", Arial, sans-serif; 4 | } 5 | 6 | aside { 7 | border-top: 1px solid gray; 8 | padding-top: 20px; 9 | } 10 | 11 | dl { 12 | display: grid; 13 | grid-column-gap: 15px; 14 | grid-row-gap: 2px; 15 | grid-template-columns: 2fr 1fr; 16 | max-width: 300px; 17 | } 18 | 19 | dt { 20 | text-align: right; 21 | } 22 | 23 | dd { 24 | margin: 0; 25 | } 26 | 27 | #app { 28 | display: grid; 29 | grid-column-gap: 20px; 30 | grid-gap: 20px; 31 | grid-row-gap: 10px; 32 | margin: 20px auto; 33 | min-width: 500px; 34 | } 35 | 36 | #app { 37 | outline: 3px dashed orangered; 38 | padding: 15px; 39 | } 40 | 41 | @media only screen and (max-width: 999px) { 42 | #app { 43 | grid-template-areas: 44 | "store basket" 45 | "image name" 46 | "image options" 47 | "image buy" 48 | "reco reco"; 49 | grid-template-columns: 4fr 3fr; 50 | } 51 | } 52 | 53 | @media only screen and (min-width: 1000px) { 54 | #app { 55 | grid-template-areas: 56 | "store basket reco" 57 | "image name reco" 58 | "image options reco" 59 | "image buy reco"; 60 | grid-template-columns: 4fr 3fr 200px; 61 | width: 1000px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/mf-green/src/product-recommendations.tsx: -------------------------------------------------------------------------------- 1 | import "./style/recommendations.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import reco1 from "./images/reco_1.jpg"; 5 | import reco2 from "./images/reco_2.jpg"; 6 | import reco3 from "./images/reco_3.jpg"; 7 | import reco4 from "./images/reco_4.jpg"; 8 | import reco5 from "./images/reco_5.jpg"; 9 | import reco6 from "./images/reco_6.jpg"; 10 | import reco7 from "./images/reco_7.jpg"; 11 | import reco8 from "./images/reco_8.jpg"; 12 | import reco9 from "./images/reco_9.jpg"; 13 | 14 | const recos = { 15 | 1: reco1, 16 | 2: reco2, 17 | 3: reco3, 18 | 4: reco4, 19 | 5: reco5, 20 | 6: reco6, 21 | 7: reco7, 22 | 8: reco8, 23 | 9: reco9, 24 | }; 25 | 26 | const allRecommendations = { 27 | porsche: ["3", "5", "6"], 28 | fendt: ["3", "6", "4"], 29 | eicher: ["1", "8", "7"], 30 | }; 31 | 32 | const Recommendations = ({ sku = "porsche" }) => { 33 | const recommendations = allRecommendations[sku] || allRecommendations.porsche; 34 | 35 | return ( 36 | <> 37 |

Related Products

38 | {recommendations.map((id) => ( 39 | {`Recommendation 40 | ))} 41 | 42 | ); 43 | }; 44 | 45 | export default Recommendations; 46 | 47 | export function renderRecommendations(container: HTMLElement) { 48 | ReactDOM.render(, container); 49 | } 50 | -------------------------------------------------------------------------------- /packages/esbuild/src/collect-exports.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { parse } from "acorn"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | type Node = any; 6 | 7 | type visitFn = (node: Node) => void; 8 | 9 | export function collectExports(path: string) { 10 | const src = fs.readFileSync(path, "utf8"); 11 | 12 | const parseTree = parse(src, { 13 | ecmaVersion: "latest", 14 | allowHashBang: true, 15 | sourceType: "module", 16 | }); 17 | 18 | let hasDefaultExport = false; 19 | let hasFurtherExports = false; 20 | let defaultExportName = ""; 21 | const exports = new Set(); 22 | 23 | traverse(parseTree, (node) => { 24 | if ( 25 | node.type === "AssignmentExpression" && 26 | node?.left?.object?.name === "exports" // && 27 | ) { 28 | exports.add(node.left.property?.name); 29 | return; 30 | } 31 | 32 | if (hasDefaultExport && hasFurtherExports) { 33 | return; 34 | } 35 | 36 | if (node.type !== "ExportNamedDeclaration") { 37 | return; 38 | } 39 | 40 | if (!node.specifiers) { 41 | hasFurtherExports = true; 42 | return; 43 | } 44 | 45 | for (const s of node.specifiers) { 46 | if (isDefaultExport(s)) { 47 | defaultExportName = s?.local?.name; 48 | hasDefaultExport = true; 49 | } else { 50 | hasFurtherExports = true; 51 | } 52 | } 53 | }); 54 | 55 | return { 56 | hasDefaultExport, 57 | hasFurtherExports, 58 | defaultExportName, 59 | exports: [...exports], 60 | }; 61 | } 62 | 63 | function traverse(node: Node, visit: visitFn) { 64 | visit(node); 65 | for (const key in node) { 66 | const prop = node[key]; 67 | if (prop && typeof prop === "object") { 68 | traverse(prop as Node, visit); 69 | } else if (Array.isArray(prop)) { 70 | for (const sub of prop) { 71 | traverse(sub, visit); 72 | } 73 | } 74 | } 75 | } 76 | 77 | function isDefaultExport(exportSpecifier: Node) { 78 | return ( 79 | exportSpecifier.exported?.type === "Identifier" && 80 | exportSpecifier.exported?.name === "default" 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /packages/mf-red/src/product-page.tsx: -------------------------------------------------------------------------------- 1 | import "./style/product-page.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import tractorRed from "./images/tractor-red.jpg"; 5 | import tractorBlue from "./images/tractor-blue.jpg"; 6 | import tractorGreen from "./images/tractor-green.jpg"; 7 | import tractorRedThumb from "./images/tractor-red-thumb.jpg"; 8 | import tractorBlueThumb from "./images/tractor-blue-thumb.jpg"; 9 | import tractorGreenThumb from "./images/tractor-green-thumb.jpg"; 10 | 11 | const product = { 12 | name: "Tractor", 13 | variants: [ 14 | { 15 | sku: "porsche", 16 | color: "red", 17 | name: "Porsche-Diesel Master 419", 18 | image: tractorRed, 19 | thumb: tractorRedThumb, 20 | price: "66,00 €", 21 | }, 22 | { 23 | sku: "fendt", 24 | color: "green", 25 | name: "Fendt F20 Dieselroß", 26 | image: tractorGreen, 27 | thumb: tractorGreenThumb, 28 | price: "54,00 €", 29 | }, 30 | { 31 | sku: "eicher", 32 | color: "blue", 33 | name: "Eicher Diesel 215/16", 34 | image: tractorBlue, 35 | thumb: tractorBlueThumb, 36 | price: "58,00 €", 37 | }, 38 | ], 39 | }; 40 | 41 | const BasketInfo = window.loadComponent("mf-blue", "./basketInfo"); 42 | const BuyButton = window.loadComponent("mf-blue", "./buyButton"); 43 | const ProductRecommendations = window.loadComponent( 44 | "mf-green", 45 | "./recommendations" 46 | ); 47 | 48 | function getCurrent(sku: string) { 49 | return product.variants.find((v) => v.sku === sku) || product.variants[0]; 50 | } 51 | 52 | const ProductPage = () => { 53 | const [sku, setSku] = React.useState("porsche"); 54 | const current = getCurrent(sku); 55 | 56 | return ( 57 | 58 |

The Model Store

59 |
60 | 61 |
62 |
63 |
64 | {current.name} 65 |
66 |
67 |

68 | {product.name} {current.name} 69 |

70 |
71 | {product.variants.map((variant) => ( 72 | 80 | ))} 81 |
82 |
83 | 84 |
85 |
86 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default ProductPage; 93 | 94 | export function renderProductPage(container: HTMLElement) { 95 | ReactDOM.render(, container); 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Piral Logo](https://github.com/smapiot/piral/raw/develop/docs/assets/logo.png)](https://piral.io) 2 | 3 | # [Piral Cloud Sample](https://piral.cloud) · [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/smapiot/piral/blob/main/LICENSE) [![Gitter Chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/piral-io/community) 4 | 5 | > Sample project to illustrate a micro frontend discovery with Native Federation. 6 | 7 | :zap: Use the Piral Feed Service for generic micro frontend discovery in the context of Native Federation-based micro frontends. 8 | 9 | Feel free to play around with the code using any of these cloud IDEs. 10 | 11 | ## ☁ Open in the Cloud 12 | [![Open in VS Code](https://img.shields.io/badge/Open%20in-VS%20Code-blue?logo=visualstudiocode)](https://vscode.dev/github/piral-samples/piral-cloud-native-federation-demo) 13 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/piral-samples/piral-cloud-native-federation-demo) 14 | [![Open in CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/embed/react-markdown-preview-co1mj?fontsize=14&hidenavigation=1&theme=dark) 15 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/piral-samples/piral-cloud-native-federation-demo) 16 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/DanielSaromo/Pacman_UCB_Behavioral_Cloning?template=node&title=ngx-vcard%20Example) 17 | [![Open in Repl.it](https://replit.com/badge/github/withastro/astro)](https://replit.com/github/piral-samples/piral-cloud-native-federation-demo) 18 | [![Open in Glitch](https://img.shields.io/badge/Open%20in-Glitch-blue?logo=glitch)](https://glitch.com/edit/#!/import/github/piral-samples/piral-cloud-native-federation-demo) 19 | [![Open in Codeanywhere](https://codeanywhere.com/img/open-in-codeanywhere-btn.svg)](https://app.codeanywhere.com/#https://github.com/piral-samples/piral-cloud-native-federation-demo) 20 | 21 | You can also visit this demo at [cloud-native-federation.samples.piral.cloud/](https://cloud-native-federation.samples.piral.cloud/). 22 | 23 | ## Structure 24 | 25 | This repository contains the following packages / code elements: 26 | 27 | - [esbuild](./packages/esbuild/): the build adapter to use esbuild for the bundling 28 | - [host-direct](./packages/host-direct/): the host with direct MF integration - for local debugging 29 | - [host-indirect](./packages/host-indirect/): the host with indirect MF integration through a MF discovery service - for production purposes 30 | - [mf-blue](./packages/mf-blue/): the MF with the buy and basket components from the blue team 31 | - [mf-green](./packages/mf-green/): the MF with the recommendations component from the green team 32 | - [mf-red](./packages/mf-red/): the MF with the product details component from the red team 33 | - [shared](./packages/shared/): the shared loader that enables cross-MF component sharing 34 | 35 | ## Publishing to a Discovery Service 36 | 37 | Following the same principles as outlined in [this article](https://dev.to/florianrappl/micro-frontend-discovery-the-driver-for-scalability-oai) the repository uses the `publish-microfrontend` helper CLI to publish individual packages to the Piral Feed Service. 38 | 39 | The Piral Feed Service recognizes the format and shows the micro frontends already as Native Federation modules in the feed. 40 | 41 | ![Feed with Native Federation modules](./native-feed.png) 42 | 43 | As feed representation we use the "Native Federation Remote". In this example the representation will look like this: 44 | 45 | ```json 46 | { 47 | "mf-blue": "https://assets.piral.cloud/pilets/native-federation-demo/mf-blue/1.0.0/remoteEntry.json", 48 | "mf-green": "https://assets.piral.cloud/pilets/native-federation-demo/mf-green/1.0.0/remoteEntry.json", 49 | "mf-red": "https://assets.piral.cloud/pilets/native-federation-demo/mf-red/1.0.3/remoteEntry.json" 50 | } 51 | ``` 52 | 53 | The feed is inserted [in the root module of the host-indirect package](./packages/host-indirect/src/index.ts). 54 | 55 | ## License 56 | 57 | Piral and this sample code is released using the MIT license. For more information see the [license file](./LICENSE). 58 | -------------------------------------------------------------------------------- /packages/esbuild/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuildAdapter, 3 | BuildAdapterOptions, 4 | BuildResult, 5 | } from "@softarc/native-federation/build"; 6 | import * as esbuild from "esbuild"; 7 | import * as fs from "fs"; 8 | import { rollup } from "rollup"; 9 | import resolve from "@rollup/plugin-node-resolve"; 10 | import { externals } from "rollup-plugin-node-externals"; 11 | import { collectExports } from "./collect-exports"; 12 | import path from "path"; 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-var-requires 15 | const commonjs = require("@rollup/plugin-commonjs"); 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-var-requires 18 | const replace = require("@rollup/plugin-replace"); 19 | 20 | export const esBuildAdapter: BuildAdapter = createEsBuildAdapter({ 21 | plugins: [], 22 | }); 23 | 24 | export type ReplacementConfig = { 25 | file: string; 26 | }; 27 | 28 | export interface EsBuildAdapterConfig { 29 | plugins: esbuild.Plugin[]; 30 | fileReplacements?: Record; 31 | skipRollup?: boolean; 32 | compensateExports?: RegExp[]; 33 | loader?: { [ext: string]: esbuild.Loader }; 34 | } 35 | 36 | export function createEsBuildAdapter(config: EsBuildAdapterConfig) { 37 | if (!config.compensateExports) { 38 | config.compensateExports = [new RegExp("/react/")]; 39 | } 40 | 41 | return async (options: BuildAdapterOptions): Promise => { 42 | const { entryPoints, external, outdir, hash } = options; 43 | 44 | // TODO: Do we need to prepare packages anymore as esbuild has evolved? 45 | 46 | for (const entryPoint of entryPoints) { 47 | const isPkg = entryPoint.fileName.includes("node_modules"); 48 | const pkgName = isPkg ? inferePkgName(entryPoint.fileName) : ""; 49 | const tmpFolder = `node_modules/.tmp/${pkgName}`; 50 | 51 | if (isPkg) { 52 | await prepareNodePackage( 53 | entryPoint.fileName, 54 | external, 55 | tmpFolder, 56 | config, 57 | !!options.dev 58 | ); 59 | 60 | entryPoint.fileName = tmpFolder; 61 | } 62 | } 63 | 64 | const ctx = await esbuild.context({ 65 | entryPoints: entryPoints.map((ep) => ({ 66 | in: ep.fileName, 67 | out: path.parse(ep.outName).name, 68 | })), 69 | write: false, 70 | outdir, 71 | entryNames: hash ? "[name]-[hash]" : "[name]", 72 | external, 73 | loader: config.loader, 74 | bundle: true, 75 | sourcemap: options.dev, 76 | minify: !options.dev, 77 | format: "esm", 78 | target: ["esnext"], 79 | plugins: [...config.plugins], 80 | }); 81 | 82 | const result = await ctx.rebuild(); 83 | const writtenFiles = writeResult(result, outdir); 84 | ctx.dispose(); 85 | return writtenFiles.map((fileName) => ({ fileName })); 86 | 87 | // const normEntryPoint = entryPoint.replace(/\\/g, '/'); 88 | // if ( 89 | // isPkg && 90 | // config?.compensateExports?.find((regExp) => regExp.exec(normEntryPoint)) 91 | // ) { 92 | // logger.verbose('compensate exports for ' + tmpFolder); 93 | // compensateExports(tmpFolder, outfile); 94 | // } 95 | }; 96 | } 97 | 98 | function writeResult( 99 | result: esbuild.BuildResult, 100 | outdir: string 101 | ) { 102 | const outputFiles = result.outputFiles || []; 103 | const writtenFiles: string[] = []; 104 | for (const outFile of outputFiles) { 105 | const fileName = path.basename(outFile.path); 106 | const filePath = path.join(outdir, fileName); 107 | fs.writeFileSync(filePath, outFile.contents); 108 | writtenFiles.push(filePath); 109 | } 110 | 111 | return writtenFiles; 112 | } 113 | 114 | function compensateExports(entryPoint: string, outfile?: string): void { 115 | const inExports = collectExports(entryPoint); 116 | const outExports = outfile ? collectExports(outfile) : inExports; 117 | 118 | if (!outExports.hasDefaultExport || outExports.hasFurtherExports) { 119 | return; 120 | } 121 | const defaultName = outExports.defaultExportName; 122 | 123 | let exports = "/*Try to compensate missing exports*/\n\n"; 124 | for (const exp of inExports.exports) { 125 | exports += `let ${exp}$softarc = ${defaultName}.${exp};\n`; 126 | exports += `export { ${exp}$softarc as ${exp} };\n`; 127 | } 128 | 129 | const target = outfile ?? entryPoint; 130 | fs.appendFileSync(target, exports, "utf-8"); 131 | } 132 | 133 | async function prepareNodePackage( 134 | entryPoint: string, 135 | external: string[], 136 | tmpFolder: string, 137 | config: EsBuildAdapterConfig, 138 | dev: boolean 139 | ) { 140 | if (config.fileReplacements) { 141 | entryPoint = replaceEntryPoint( 142 | entryPoint, 143 | normalize(config.fileReplacements) 144 | ); 145 | } 146 | 147 | const env = dev ? "development" : "production"; 148 | 149 | const result = await rollup({ 150 | input: entryPoint, 151 | 152 | plugins: [ 153 | commonjs(), 154 | externals({ include: external }), 155 | resolve(), 156 | replace({ 157 | preventAssignment: true, 158 | values: { 159 | "process.env.NODE_ENV": `"${env}"`, 160 | }, 161 | }), 162 | ], 163 | }); 164 | 165 | await result.write({ 166 | format: "esm", 167 | file: tmpFolder, 168 | sourcemap: dev, 169 | exports: "named", 170 | }); 171 | } 172 | 173 | function inferePkgName(entryPoint: string) { 174 | return entryPoint 175 | .replace(/.*?node_modules/g, "") 176 | .replace(/[^A-Za-z0-9.]/g, "_"); 177 | } 178 | 179 | function normalize( 180 | config: Record 181 | ): Record { 182 | const result: Record = {}; 183 | for (const key in config) { 184 | if (typeof config[key] === "string") { 185 | result[key] = { 186 | file: config[key] as string, 187 | }; 188 | } else { 189 | result[key] = config[key] as ReplacementConfig; 190 | } 191 | } 192 | return result; 193 | } 194 | 195 | function replaceEntryPoint( 196 | entryPoint: string, 197 | fileReplacements: Record 198 | ): string { 199 | entryPoint = entryPoint.replace(/\\/g, "/"); 200 | 201 | for (const key in fileReplacements) { 202 | entryPoint = entryPoint.replace( 203 | new RegExp(`${key}$`), 204 | fileReplacements[key].file 205 | ); 206 | } 207 | 208 | return entryPoint; 209 | } 210 | --------------------------------------------------------------------------------