├── .github └── workflows │ ├── codeql-analysis.yml │ ├── issues-jira.yml │ ├── policy-scan.yml │ ├── sca-scan.yml │ └── secrets-scan.yml ├── .screenshots └── microfrontend-breadcrumbs.png ├── .talismanrc ├── CODEOWNERS ├── README.md ├── docker-compose.yaml ├── pizza-app ├── .babelrc ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── nginx.conf ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── all-pizzas.tsx │ │ ├── app.tsx │ │ ├── nav-layout.tsx │ │ ├── pizza-menu.tsx │ │ └── vegan-pizza.tsx │ ├── index.html │ ├── index.tsx │ ├── lib │ │ ├── configureStore.ts │ │ ├── microfrontend-meta-context.ts │ │ └── rootReducer.ts │ └── styles │ │ └── main.scss ├── vendor.js ├── vendor │ └── vendor-manifest.json ├── webpack.config.dll.js └── webpack.config.js └── restaurant ├── .babelrc ├── .gitignore ├── Dockerfile ├── LICENSE ├── nginx.conf ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.tsx │ ├── components │ │ ├── home-page.tsx │ │ ├── layout.tsx │ │ ├── micro-frontend-container.tsx │ │ └── sandwich-page.tsx │ └── microfrontends.ts ├── index.html ├── index.tsx └── styles │ └── main.scss ├── vendor.js ├── vendor └── vendor-manifest.json ├── webpack.config.dll.js └── webpack.config.js /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | pull_request: 16 | # The branches below must be a subset of the branches above 17 | branches: '*' 18 | 19 | jobs: 20 | analyze: 21 | name: Analyze 22 | runs-on: ubuntu-latest 23 | permissions: 24 | actions: read 25 | contents: read 26 | security-events: write 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 33 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 34 | 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v3 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v2 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | 48 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 49 | # queries: security-extended,security-and-quality 50 | 51 | 52 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 53 | # If this step fails, then you should remove it and run the build manually (see below) 54 | - name: Autobuild 55 | uses: github/codeql-action/autobuild@v2 56 | 57 | # ℹ️ Command-line programs to run using the OS shell. 58 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 59 | 60 | # If the Autobuild fails above, remove it and uncomment the following three lines. 61 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 62 | 63 | # - run: | 64 | # echo "Run, Build Application using script" 65 | # ./location_of_script_within_repo/buildscript.sh 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v2 69 | -------------------------------------------------------------------------------- /.github/workflows/issues-jira.yml: -------------------------------------------------------------------------------- 1 | name: Create Jira Ticket for Github Issue 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | issue-jira: 9 | runs-on: ubuntu-latest 10 | steps: 11 | 12 | - name: Login to Jira 13 | uses: atlassian/gajira-login@master 14 | env: 15 | JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} 16 | JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} 17 | JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} 18 | 19 | - name: Create Jira Issue 20 | id: create_jira 21 | uses: atlassian/gajira-create@master 22 | with: 23 | project: ${{ secrets.JIRA_PROJECT }} 24 | issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} 25 | summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }} 26 | description: | 27 | *GitHub Issue:* ${{ github.event.issue.html_url }} 28 | 29 | *Description:* 30 | ${{ github.event.issue.body }} 31 | fields: "${{ secrets.ISSUES_JIRA_FIELDS }}" -------------------------------------------------------------------------------- /.github/workflows/policy-scan.yml: -------------------------------------------------------------------------------- 1 | name: Checks the security policy and configurations 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | jobs: 6 | security-policy: 7 | if: github.event.repository.visibility == 'public' 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | shell: bash 12 | steps: 13 | - uses: actions/checkout@master 14 | - name: Checks for SECURITY.md policy file 15 | run: | 16 | if ! [[ -f "SECURITY.md" || -f ".github/SECURITY.md" ]]; then exit 1; fi 17 | security-license: 18 | if: github.event.repository.visibility == 'public' 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | shell: bash 23 | steps: 24 | - uses: actions/checkout@master 25 | - name: Checks for License file 26 | run: | 27 | expected_license_files=("LICENSE" "LICENSE.txt" "LICENSE.md" "License.txt") 28 | license_file_found=false 29 | current_year=$(date +"%Y") 30 | 31 | for license_file in "${expected_license_files[@]}"; do 32 | if [ -f "$license_file" ]; then 33 | license_file_found=true 34 | # check the license file for the current year, if not exists, exit with error 35 | if ! grep -q "$current_year" "$license_file"; then 36 | echo "License file $license_file does not contain the current year." 37 | exit 2 38 | fi 39 | break 40 | fi 41 | done 42 | 43 | if [ "$license_file_found" = false ]; then 44 | echo "No license file found. Please add a license file to the repository." 45 | exit 1 46 | fi -------------------------------------------------------------------------------- /.github/workflows/sca-scan.yml: -------------------------------------------------------------------------------- 1 | name: Source Composition Analysis Scan 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | jobs: 6 | security-sca: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: Run Snyk to check for vulnerabilities 11 | uses: snyk/actions/node@master 12 | env: 13 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 14 | with: 15 | args: --all-projects --fail-on=all 16 | -------------------------------------------------------------------------------- /.github/workflows/secrets-scan.yml: -------------------------------------------------------------------------------- 1 | name: Secrets Scan 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened] 5 | jobs: 6 | security-secrets: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | fetch-depth: '2' 12 | ref: '${{ github.event.pull_request.head.ref }}' 13 | - run: | 14 | git reset --soft HEAD~1 15 | - name: Install Talisman 16 | run: | 17 | # Download Talisman 18 | wget https://github.com/thoughtworks/talisman/releases/download/v1.37.0/talisman_linux_amd64 -O talisman 19 | 20 | # Checksum verification 21 | checksum=$(sha256sum ./talisman | awk '{print $1}') 22 | if [ "$checksum" != "8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4" ]; then exit 1; fi 23 | 24 | # Make it executable 25 | chmod +x talisman 26 | - name: Run talisman 27 | run: | 28 | # Run Talisman with the pre-commit hook 29 | ./talisman --githook pre-commit -------------------------------------------------------------------------------- /.screenshots/microfrontend-breadcrumbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentstack/micro-frontend-example/20a1652cd81d712235c27f23433b8451f6c37865/.screenshots/microfrontend-breadcrumbs.png -------------------------------------------------------------------------------- /.talismanrc: -------------------------------------------------------------------------------- 1 | fileignoreconfig: 2 | - filename: .github/workflows/secrets-scan.yml 3 | ignore_detectors: 4 | - filecontent 5 | version: "1.0" -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @contentstack/security-admin 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # micro-frontend example 2 | 3 | This repository serves to demonstrate an example of micro-frontend implementation. It consists of the following apps in the respective directories: 4 | 5 | - restaurant (container app) 6 | - pizza-app (micro-frontend app) 7 | 8 | The example is of a restaurant menu, where the `pizza` and `sandwich` are two sections. The `pizza` section is served as a micro-frontend while the `sandwich` section is not. 9 | 10 | ## Getting Started 11 | 12 | ### For the impatient: `docker-compose` 13 | 14 | You can use `docker-compose` to start the setup quickly. Run the following command: 15 | 16 | ``` 17 | $ docker-compose up 18 | ``` 19 | 20 | ### Starting the container app: restaurant 21 | 22 | Navigate to the `restaurant` directory and run the following: 23 | 24 | ``` 25 | $ npm install 26 | $ npm start 27 | ``` 28 | 29 | The app will be available at `http://localhost:8080`. 30 | 31 | ### Starting the micro-frontend: `pizza-app` 32 | 33 | You can similarly navigate to the `pizza-app` directory and run the following commands to start the micro-frontend. It will serve up the micro-frontend JavaScript bundle at `http://localhost:8081/app.bundle.js`. 34 | 35 | ``` 36 | $ npm install 37 | $ npm start 38 | ``` 39 | 40 | ## Rendering outside the micro-frontend frame 41 | 42 | In this example, you can see how the `pizza-app` micro-frontend needs to control breadcrumbs that lie outside the micro-frontend frame: 43 | 44 | ![micro-frontend-breadcrumbs](https://github.com/contentstack/micro-frontend-example/blob/main/.screenshots/microfrontend-breadcrumbs.png?raw=true) 45 | 46 | It does this by using [React Portal] APIs. When rendering the micro-frontend, the container app relinquishes control of the breadcrumbs to the micro-frontend. It passes the `div` ID of the breadcrumbs section to the micro-frontend, and the micro-frontend in turn uses React Portal APIs to render it's own breadcrumbs within that section. 47 | 48 | [React Portal]: https://reactjs.org/docs/portals.html 49 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | restaurant: 4 | build: restaurant 5 | ports: 6 | - "8080:8080" 7 | pizza-app: 8 | build: pizza-app 9 | ports: 10 | - "8081:8081" -------------------------------------------------------------------------------- /pizza-app/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/typescript", 5 | "@babel/react" 6 | ], 7 | "plugins": [ 8 | [ 9 | "inline-react-svg" 10 | ], 11 | [ 12 | "@babel/plugin-transform-regenerator" 13 | ], 14 | [ 15 | "@babel/plugin-syntax-dynamic-import" 16 | ], 17 | [ 18 | "transform-class-properties" 19 | ], 20 | [ 21 | "@babel/plugin-transform-arrow-functions", 22 | { 23 | "spec": true 24 | } 25 | ] 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /pizza-app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist -------------------------------------------------------------------------------- /pizza-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app 3 | WORKDIR /home/node/app 4 | USER node 5 | COPY --chown=node:node package*.json ./ 6 | RUN npm install 7 | COPY --chown=node:node . . 8 | RUN npm run build 9 | 10 | FROM nginx:1.18.0-alpine 11 | COPY nginx.conf /etc/nginx/conf.d/default.conf 12 | COPY --from=0 /home/node/app/dist /usr/share/nginx/html -------------------------------------------------------------------------------- /pizza-app/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abhinav Paliwal 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 | -------------------------------------------------------------------------------- /pizza-app/README.md: -------------------------------------------------------------------------------- 1 | # pizza-app 2 | 3 | This is the microfrontend app 4 | 5 | ## Getting Started 6 | 7 | To launch locally and run the UI: 8 | 9 | ``` 10 | $ npm start 11 | ``` 12 | 13 | The UI will be available at http://localhost:8080. 14 | 15 | Create a build using: 16 | 17 | ``` 18 | $ npm run build 19 | ``` 20 | 21 | The static files will be generated in ./dist 22 | 23 | -------------------------------------------------------------------------------- /pizza-app/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8081; 3 | listen [::]:8081; 4 | server_name localhost; 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | try_files $uri /index.html; 10 | add_header Access-Control-Allow-Origin *; 11 | } 12 | 13 | error_page 500 502 503 504 /50x.html; 14 | location = /50x.html { 15 | root /usr/share/nginx/html; 16 | } 17 | } -------------------------------------------------------------------------------- /pizza-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pizza-ui", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "prebuild": "webpack --config webpack.config.dll.js", 7 | "prestart": "webpack --config webpack.config.dll.js", 8 | "build": "webpack --progress --colors", 9 | "start": "webpack-dev-server", 10 | "test": "jest", 11 | "lint": "eslint src/**/*.{ts,tsx} --fix" 12 | }, 13 | "author": "contentstack", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bootstrap": "^5.0.1", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-redux": "^7.2.1", 20 | "react-router-dom": "^5.2.0", 21 | "redux": "^4.0.5", 22 | "yup": "^0.29.3" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.12.7", 26 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 27 | "@babel/plugin-transform-arrow-functions": "^7.10.4", 28 | "@babel/plugin-transform-regenerator": "^7.10.4", 29 | "@babel/polyfill": "^7.10.4", 30 | "@babel/preset-env": "^7.12.7", 31 | "@babel/preset-react": "^7.10.4", 32 | "@babel/preset-typescript": "^7.10.4", 33 | "@types/enzyme-adapter-react-16": "^1.0.6", 34 | "@types/react-dom": "^16.9.8", 35 | "@types/react-redux": "^7.1.9", 36 | "@types/react-router-dom": "^5.1.5", 37 | "@typescript-eslint/eslint-plugin": "^3.9.1", 38 | "@typescript-eslint/parser": "^3.9.1", 39 | "add-asset-html-webpack-plugin": "^3.1.3", 40 | "babel-loader": "^8.2.1", 41 | "babel-plugin-inline-react-svg": "^2.0.1", 42 | "babel-plugin-module-resolver": "^4.0.0", 43 | "babel-plugin-transform-class-properties": "^6.24.1", 44 | "clean-webpack-plugin": "^3.0.0", 45 | "copy-webpack-plugin": "^6.0.3", 46 | "css-loader": "^5.2.6", 47 | "eslint": "^7.7.0", 48 | "eslint-config-prettier": "^6.11.0", 49 | "eslint-plugin-prettier": "^3.1.4", 50 | "eslint-plugin-react": "^7.20.6", 51 | "html-webpack-plugin": "^4.3.0", 52 | "prettier": "^2.0.5", 53 | "sass-loader": "^12.0.0", 54 | "style-loader": "^2.0.0", 55 | "typescript": "^3.9.7", 56 | "webpack": "^5.38.1", 57 | "webpack-cli": "^3.3.12", 58 | "webpack-dev-server": "^3.11.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pizza-app/src/app/all-pizzas.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const AllPizzas = () => { 4 | return ( 5 |
6 |

All pizza options

7 | 8 |
9 |
10 |
11 |
Bbq
12 | Choose 13 |
14 |
15 | 16 |
17 |
18 |
Pepperoni
19 | Choose 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 |
Vegan garden
28 | Choose 29 |
30 |
31 | 32 |
33 |
34 |
Vegan cheese
35 | Choose 36 |
37 |
38 |
39 |
40 | ) 41 | } -------------------------------------------------------------------------------- /pizza-app/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactComponentElement, 3 | useContext, 4 | } from 'react'; 5 | 6 | import { 7 | Route, 8 | Switch, 9 | } from 'react-router-dom'; 10 | 11 | import { MicrofrontendMetaContext } from '../lib/microfrontend-meta-context'; 12 | import { AllPizzas } from './all-pizzas'; 13 | import { NavLayout } from './nav-layout'; 14 | import { PizzaMenu } from './pizza-menu'; 15 | import { VeganPizza } from './vegan-pizza'; 16 | 17 | export type Crumb = { 18 | name: string, 19 | link?: string, 20 | }; 21 | 22 | const HomeCrumb: Crumb = { 23 | name: 'Home', 24 | link: '/', 25 | }; 26 | 27 | const PizzaCrumb: Crumb = { 28 | name: 'Pizzas', 29 | link: '/pizza', 30 | }; 31 | 32 | const VeganPizzaCrumb: Crumb = { 33 | name: 'Vegan Pizzas', 34 | link: '/pizza/vegan', 35 | }; 36 | 37 | const AllPizzaCrumb: Crumb = { 38 | name: 'All Pizzas', 39 | link: '/pizza/all-pizzas', 40 | }; 41 | 42 | const baseCrumbs = [HomeCrumb, PizzaCrumb]; 43 | 44 | export const App = (): ReactComponentElement => { 45 | const microfrontendMeta = useContext(MicrofrontendMetaContext); 46 | 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /pizza-app/src/app/nav-layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | 6 | import { MicrofrontendMetaContext } from '../lib/microfrontend-meta-context'; 7 | import { Crumb } from './app'; 8 | 9 | export const NavLayout: React.FunctionComponent<{ crumbs: Crumb[] }> = ({ children, crumbs }) => { 10 | const microfrontendMeta = useContext(MicrofrontendMetaContext); 11 | 12 | return ( 13 | <> 14 | { 15 | ReactDOM.createPortal( 16 |
    17 | { 18 | crumbs.map(crumb => ( 19 |
  1. 20 | {crumb.name} 21 |
  2. 22 | )) 23 | } 24 |
, 25 | document.getElementById(microfrontendMeta.layoutNavId), 26 | ) 27 | } 28 | {children} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /pizza-app/src/app/pizza-menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | export const PizzaMenu = ({ relativeUrl }) => { 6 | return ( 7 |
8 |

Choose the pizza you like

9 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /pizza-app/src/app/vegan-pizza.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const VeganPizza = () => { 4 | return ( 5 |
6 |

Vegan pizza options

7 | 8 |
9 |
10 |
11 |
Vegan garden
12 | Choose 13 |
14 |
15 | 16 |
17 |
18 |
Vegan cheese
19 | Choose 20 |
21 |
22 |
23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /pizza-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 |
12 |
13 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /pizza-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { 5 | createBrowserHistory, 6 | History, 7 | } from 'history'; 8 | import { Provider } from 'react-redux'; 9 | import { Router } from 'react-router-dom'; 10 | 11 | import { App } from './app/app'; 12 | import store from './lib/configureStore'; 13 | import { 14 | MicrofrontendMeta, 15 | MicrofrontendMetaContext, 16 | } from './lib/microfrontend-meta-context'; 17 | 18 | declare global { 19 | interface Window { 20 | renderPizza: ( 21 | containerID?: string, 22 | history?: History, 23 | microfrontendMeta?: { relativeUrl: string; layoutNavId: string } 24 | ) => void; 25 | } 26 | } 27 | // Update the name of your app over here and in index.html 28 | window.renderPizza = ( 29 | containerId = 'root', 30 | history = createBrowserHistory(), 31 | microfrontendMeta: MicrofrontendMeta, 32 | ) => { 33 | ReactDOM.render( 34 | 35 | 36 | 37 | 38 | 39 | 40 | , 41 | document.getElementById(containerId) 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /pizza-app/src/lib/configureStore.ts: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | createStore, 4 | } from 'redux'; 5 | 6 | import rootReducer from './rootReducer'; 7 | 8 | declare global { 9 | interface Window { 10 | __REDUX_DEVTOOLS_EXTENSION__?: typeof compose; 11 | } 12 | } 13 | 14 | const configureStore = () => { 15 | const store = createStore( 16 | rootReducer, 17 | window.__REDUX_DEVTOOLS_EXTENSION__ 18 | && compose(window.__REDUX_DEVTOOLS_EXTENSION__()) 19 | ); 20 | return store; 21 | }; 22 | 23 | const store = configureStore(); 24 | 25 | export default store; 26 | -------------------------------------------------------------------------------- /pizza-app/src/lib/microfrontend-meta-context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type MicrofrontendMeta = { 4 | relativeUrl: string, 5 | layoutNavId: string, 6 | } 7 | 8 | export const MicrofrontendMetaContext = React.createContext({ 9 | relativeUrl: '/pizza', 10 | layoutNavId: 'layout-nav' 11 | }); 12 | -------------------------------------------------------------------------------- /pizza-app/src/lib/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | const RootReducer = combineReducers({ 4 | }); 5 | 6 | export default RootReducer; 7 | -------------------------------------------------------------------------------- /pizza-app/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; -------------------------------------------------------------------------------- /pizza-app/vendor.js: -------------------------------------------------------------------------------- 1 | //these libraries would be shared accross the micro-frontend apps dont major upgrade the versions of these packages 2 | require('react'); 3 | require('react-dom'); 4 | require('react-redux'); 5 | require('react-router'); 6 | require('redux'); 7 | -------------------------------------------------------------------------------- /pizza-app/vendor/vendor-manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"vendor","content":{"./vendor.js":{"id":"./vendor.js","buildMeta":{}}}} -------------------------------------------------------------------------------- /pizza-app/webpack.config.dll.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | entry: { 7 | vendor: [path.join(__dirname, 'vendor.js')], 8 | }, 9 | mode: 'development', 10 | output: { 11 | path: path.join(__dirname, 'build'), 12 | filename: '[name].js', 13 | library: '[name]', 14 | }, 15 | plugins: [ 16 | new webpack.DllPlugin({ 17 | path: path.join(__dirname, 'vendor', '[name]-manifest.json'), 18 | name: '[name]', 19 | }), 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /pizza-app/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | const pkg = require('./package.json'); 6 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); 7 | module.exports = { 8 | entry: { 9 | app: ['@babel/polyfill', './src/index.tsx'], 10 | }, 11 | output: { 12 | path: path.resolve(process.cwd(), 'dist'), 13 | filename: `[name].bundle.js?v=${pkg.version}`, 14 | publicPath: '/', 15 | }, 16 | stats: { warnings: false }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(ts|tsx)$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | { 27 | test: /\.css$/, 28 | use: ['style-loader', 'css-loader', 'sass-loader'], 29 | }, 30 | { 31 | test: /\.(png|jpg|jpeg|gif|svg|ttf|eot)$/, 32 | include: [path.resolve(__dirname, '../src')], 33 | use: [ 34 | { 35 | loader: 'file-loader', 36 | options: { 37 | name: '[name].[ext]', 38 | outputPath: 'static/images/', 39 | publicPath: './static/images', 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | 47 | resolve: { 48 | extensions: ['.ts', '.tsx', '.js', '.json', '.css'], 49 | }, 50 | 51 | devServer: { 52 | host: '0.0.0.0', 53 | hot: true, 54 | open: true, 55 | useLocalIp: true, 56 | historyApiFallback: true, 57 | headers: { 58 | 'Access-Control-Allow-Origin': '*', 59 | } 60 | }, 61 | 62 | plugins: [ 63 | new CleanWebpackPlugin(), 64 | new webpack.DllReferencePlugin({ 65 | context: __dirname, 66 | manifest: require('./vendor/vendor-manifest.json'), 67 | }), 68 | new AddAssetHtmlPlugin({ 69 | filepath: path.resolve(__dirname, './build/vendor.js'), 70 | }), 71 | ], 72 | }; 73 | -------------------------------------------------------------------------------- /restaurant/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/env", 4 | "@babel/typescript", 5 | "@babel/react" 6 | ], 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-transform-regenerator" 10 | ], 11 | [ 12 | "@babel/plugin-syntax-dynamic-import" 13 | ], 14 | [ 15 | "transform-class-properties" 16 | ], 17 | [ 18 | "@babel/plugin-transform-arrow-functions", 19 | { 20 | "spec": true 21 | } 22 | ] 23 | ] 24 | } -------------------------------------------------------------------------------- /restaurant/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist -------------------------------------------------------------------------------- /restaurant/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12-alpine 2 | RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app 3 | WORKDIR /home/node/app 4 | USER node 5 | COPY --chown=node:node package*.json ./ 6 | RUN npm install 7 | COPY --chown=node:node . . 8 | RUN npm run build 9 | 10 | FROM nginx:1.18.0-alpine 11 | COPY nginx.conf /etc/nginx/conf.d/default.conf 12 | COPY --from=0 /home/node/app/dist /usr/share/nginx/html -------------------------------------------------------------------------------- /restaurant/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Abhinav Paliwal 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 | -------------------------------------------------------------------------------- /restaurant/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | listen [::]:8080; 4 | server_name localhost; 5 | 6 | location / { 7 | root /usr/share/nginx/html; 8 | index index.html index.htm; 9 | try_files $uri /index.html; 10 | } 11 | 12 | error_page 500 502 503 504 /50x.html; 13 | location = /50x.html { 14 | root /usr/share/nginx/html; 15 | } 16 | } -------------------------------------------------------------------------------- /restaurant/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microfrontend-container-ui", 3 | "version": "0.0.1", 4 | "description": "", 5 | "scripts": { 6 | "prebuild": "webpack --config webpack.config.dll.js", 7 | "prestart": "webpack --config webpack.config.dll.js", 8 | "build": "webpack --progress --colors", 9 | "start": "webpack-dev-server", 10 | "test": "jest", 11 | "lint": "eslint src/**/*.{ts,tsx} --fix" 12 | }, 13 | "author": "contentstack", 14 | "license": "ISC", 15 | "dependencies": { 16 | "bootstrap": "^5.0.1", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-redux": "^7.2.1", 20 | "react-router-dom": "^5.2.0", 21 | "yup": "^0.29.3" 22 | }, 23 | "devDependencies": { 24 | "@babel/core": "^7.12.7", 25 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 26 | "@babel/plugin-transform-arrow-functions": "^7.10.4", 27 | "@babel/plugin-transform-regenerator": "^7.10.4", 28 | "@babel/polyfill": "^7.10.4", 29 | "@babel/preset-env": "^7.12.7", 30 | "@babel/preset-react": "^7.10.4", 31 | "@babel/preset-typescript": "^7.10.4", 32 | "@types/enzyme-adapter-react-16": "^1.0.6", 33 | "@types/react-dom": "^16.9.8", 34 | "@types/react-redux": "^7.1.9", 35 | "@types/react-router-dom": "^5.1.5", 36 | "@typescript-eslint/eslint-plugin": "^3.9.1", 37 | "@typescript-eslint/parser": "^3.9.1", 38 | "add-asset-html-webpack-plugin": "^3.2.0", 39 | "babel-loader": "^8.2.1", 40 | "babel-plugin-module-resolver": "^4.0.0", 41 | "babel-plugin-transform-class-properties": "^6.24.1", 42 | "clean-webpack-plugin": "^3.0.0", 43 | "copy-webpack-plugin": "^6.0.3", 44 | "css-loader": "^5.2.6", 45 | "eslint": "^7.7.0", 46 | "eslint-config-prettier": "^6.11.0", 47 | "eslint-plugin-prettier": "^3.1.4", 48 | "eslint-plugin-react": "^7.20.6", 49 | "html-webpack-plugin": "^5.3.1", 50 | "postcss": "^8.3.0", 51 | "postcss-loader": "^5.3.0", 52 | "prettier": "^2.0.5", 53 | "sass": "^1.34.1", 54 | "sass-loader": "^12.0.0", 55 | "style-loader": "^2.0.0", 56 | "typescript": "^3.9.7", 57 | "webpack": "^5.38.1", 58 | "webpack-cli": "^3.3.12", 59 | "webpack-dev-server": "^3.11.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /restaurant/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactComponentElement } from 'react'; 2 | 3 | import { 4 | Route, 5 | Switch, 6 | } from 'react-router-dom'; 7 | 8 | import { HomePage } from './components/home-page'; 9 | import { Layout } from './components/layout'; 10 | import { MicroFrontendContainer } from './components/micro-frontend-container'; 11 | import { SandwichPage } from './components/sandwich-page'; 12 | import { microfrontends } from './microfrontends'; 13 | 14 | export const App = (): ReactComponentElement => { 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | { 22 | microfrontends.map(microfrontend => ( 23 | 24 | 25 | 26 | )) 27 | } 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /restaurant/src/app/components/home-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Link } from 'react-router-dom'; 4 | 5 | export const HomePage = () => { 6 | return ( 7 | <> 8 |
9 |

Menu

10 |
    11 |
  • 12 | 13 | Pizza 14 | 15 |
  • 16 |
  • 17 | 18 | Sandwich 19 | 20 |
  • 21 |
22 |
23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /restaurant/src/app/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export enum LayoutController { 4 | CONTAINER = 'CONTAINER', 5 | MICROFRONTEND = 'MICROFRONTEND', 6 | } 7 | 8 | export const LayoutContext = React.createContext<{ 9 | layoutController: LayoutController, 10 | setLayoutController: (arg: any) => any, 11 | }>({ 12 | layoutController: LayoutController.CONTAINER, 13 | setLayoutController: () => { }, 14 | }); 15 | 16 | export const Layout: React.FunctionComponent<{}> = ({ children }) => { 17 | const [layoutController, setLayoutController] = useState(LayoutController.CONTAINER); 18 | 19 | return ( 20 | 21 | 38 | {children} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /restaurant/src/app/components/micro-frontend-container.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useContext, 3 | useEffect, 4 | } from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | 7 | import { useHistory } from 'react-router'; 8 | 9 | import { Microfrontend } from '../microfrontends'; 10 | import { 11 | LayoutContext, 12 | LayoutController, 13 | } from './layout'; 14 | 15 | const MicroFrontendContainer = ({ microfrontend }: { microfrontend: Microfrontend }) => { 16 | const history = useHistory(); 17 | const layoutContext = useContext(LayoutContext); 18 | 19 | useEffect(() => { 20 | const scriptId = `script-${microfrontend.divId}`; 21 | if (document && document.getElementById(scriptId)) { 22 | renderMicrofrontendScreen(); 23 | return cleanup; 24 | } 25 | const script = document.createElement('script'); 26 | script.id = scriptId; 27 | script.crossOrigin = ''; 28 | script.src = microfrontend.bundleLink; 29 | script.onload = renderMicrofrontendScreen; 30 | document.head.appendChild(script); 31 | 32 | return cleanup; 33 | }, []); 34 | 35 | const cleanup = () => { 36 | const microfrontendDiv = document.getElementById(microfrontend.divId); 37 | if (microfrontendDiv) { 38 | ReactDOM.unmountComponentAtNode(microfrontendDiv); 39 | } 40 | layoutContext.setLayoutController(LayoutController.CONTAINER); 41 | }; 42 | 43 | const renderMicrofrontendScreen = () => { 44 | layoutContext.setLayoutController(LayoutController.MICROFRONTEND); 45 | let microfrontendMeta = { 46 | relativeUrl: microfrontend.relativeUrl, 47 | layoutNavId: "layout-nav", 48 | }; 49 | (window as any)[microfrontend.renderMethod](microfrontend.divId, history, microfrontendMeta); 50 | }; 51 | 52 | return ( 53 | <> 54 |
55 | 56 | ) 57 | } 58 | 59 | export { MicroFrontendContainer }; -------------------------------------------------------------------------------- /restaurant/src/app/components/sandwich-page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const SandwichPage = () => { 4 | return ( 5 |
6 |

Sandwiches

7 |

This page is not a microfrontend

8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /restaurant/src/app/microfrontends.ts: -------------------------------------------------------------------------------- 1 | export type Microfrontend = { 2 | bundleLink: string, 3 | divId: string, 4 | relativeUrl: string, 5 | renderMethod: string, 6 | } 7 | 8 | export const microfrontends: Microfrontend[] = [ 9 | { 10 | bundleLink: 'http://localhost:8081/app.bundle.js', 11 | divId: 'pizza-microfrontend', 12 | relativeUrl: '/pizza', 13 | renderMethod: 'renderPizza', 14 | } 15 | ]; 16 | -------------------------------------------------------------------------------- /restaurant/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /restaurant/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles/main.scss'; 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | import { createBrowserHistory } from 'history'; 7 | import { Router } from 'react-router-dom'; 8 | 9 | import { App } from './app/app'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /restaurant/src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/scss/bootstrap"; 2 | -------------------------------------------------------------------------------- /restaurant/vendor.js: -------------------------------------------------------------------------------- 1 | //these libraries would be shared accross the micro-frontend apps dont major upgrade the versions of these packages 2 | require('react'); 3 | require('react-dom'); 4 | require('react-redux'); 5 | require('react-router'); 6 | require('redux'); 7 | -------------------------------------------------------------------------------- /restaurant/vendor/vendor-manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"vendor","content":{"./vendor.js":{"id":"./vendor.js","buildMeta":{}}}} -------------------------------------------------------------------------------- /restaurant/webpack.config.dll.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | context: __dirname, 6 | entry: { 7 | vendor: [path.join(__dirname, 'vendor.js')], 8 | }, 9 | mode: 'development', 10 | output: { 11 | path: path.join(__dirname, 'build'), 12 | filename: '[name].js', 13 | library: '[name]', 14 | }, 15 | plugins: [ 16 | new webpack.DllPlugin({ 17 | path: path.join(__dirname, 'vendor', '[name]-manifest.json'), 18 | name: '[name]', 19 | }), 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /restaurant/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 5 | const pkg = require('./package.json'); 6 | const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); 7 | module.exports = { 8 | entry: { 9 | app: ['@babel/polyfill', './src/index.tsx'], 10 | }, 11 | output: { 12 | path: path.resolve(process.cwd(), 'dist'), 13 | filename: `[name].bundle.js?v=${pkg.version}`, 14 | publicPath: '/', 15 | }, 16 | stats: { warnings: false }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(ts|tsx)$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader', 24 | }, 25 | }, 26 | { 27 | test: /\.s[ac]ss$/i, 28 | use: ['style-loader', 'css-loader', 'sass-loader'], 29 | }, 30 | { 31 | test: /\.(png|jpg|jpeg|gif|svg|ttf|eot)$/, 32 | include: [path.resolve(__dirname, '../src')], 33 | use: [ 34 | { 35 | loader: 'file-loader', 36 | options: { 37 | name: '[name].[ext]', 38 | outputPath: 'static/images/', 39 | publicPath: './static/images', 40 | }, 41 | }, 42 | ], 43 | }, 44 | ], 45 | }, 46 | 47 | resolve: { 48 | extensions: ['.ts', '.tsx', '.js', '.json', '.css'], 49 | }, 50 | 51 | devServer: { 52 | host: '0.0.0.0', 53 | port: 8080, 54 | hot: true, 55 | open: true, 56 | useLocalIp: true, 57 | historyApiFallback: true, 58 | }, 59 | 60 | plugins: [ 61 | new HtmlWebpackPlugin({ 62 | title: 'Restaurant', 63 | template: './src/index.html', 64 | inject: true, 65 | minify: { 66 | removeComments: true, 67 | collapseWhitespace: true, 68 | }, 69 | }), 70 | new CleanWebpackPlugin(), 71 | new webpack.DllReferencePlugin({ 72 | context: __dirname, 73 | manifest: require('./vendor/vendor-manifest.json'), 74 | }), 75 | new AddAssetHtmlPlugin({ 76 | filepath: path.resolve(__dirname, './build/vendor.js'), 77 | }), 78 | ], 79 | }; 80 | --------------------------------------------------------------------------------