├── .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 | 
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 |
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 | -
20 | {crumb.name}
21 |
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 |
10 | -
11 |
12 | All pizzas
13 |
14 |
15 | -
16 |
17 | Vegan
18 |
19 |
20 |
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 |
--------------------------------------------------------------------------------