├── 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 |
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 |
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 |

65 |
66 |
67 |
68 | {product.name} {current.name}
69 |
70 |
71 | {product.variants.map((variant) => (
72 |
80 | ))}
81 |
82 |
83 |
84 |
85 |
88 |
89 | );
90 | };
91 |
92 | export default ProductPage;
93 |
94 | export function renderProductPage(container: HTMLElement) {
95 | ReactDOM.render(, container);
96 | }
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://piral.io)
2 |
3 | # [Piral Cloud Sample](https://piral.cloud) · [](https://github.com/smapiot/piral/blob/main/LICENSE) [](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 | [](https://vscode.dev/github/piral-samples/piral-cloud-native-federation-demo)
13 | [](https://codespaces.new/piral-samples/piral-cloud-native-federation-demo)
14 | [](https://codesandbox.io/embed/react-markdown-preview-co1mj?fontsize=14&hidenavigation=1&theme=dark)
15 | [](https://gitpod.io/#https://github.com/piral-samples/piral-cloud-native-federation-demo)
16 | [](https://stackblitz.com/github/DanielSaromo/Pacman_UCB_Behavioral_Cloning?template=node&title=ngx-vcard%20Example)
17 | [](https://replit.com/github/piral-samples/piral-cloud-native-federation-demo)
18 | [](https://glitch.com/edit/#!/import/github/piral-samples/piral-cloud-native-federation-demo)
19 | [](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 | 
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 |
--------------------------------------------------------------------------------