├── .eslintrc.js ├── .gitignore ├── .imgbotconfig ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── azure-pipelines.yml ├── icon.png ├── icon.svg ├── lerna.json ├── markplace.md ├── package-lock.json ├── package.json ├── packages ├── all │ ├── .gitignore │ ├── index.js │ └── package.json ├── cli │ ├── .gitignore │ ├── bin │ │ └── index.js │ ├── config │ │ ├── cra-webpack-config-override.js │ │ ├── project-configs.js │ │ └── sw-config.js │ ├── package.json │ ├── scripts │ │ ├── build │ │ │ ├── all.js │ │ │ ├── index.js │ │ │ ├── library.js │ │ │ ├── library │ │ │ │ └── library.js │ │ │ ├── package.js │ │ │ └── single.js │ │ ├── create │ │ │ ├── app.js │ │ │ ├── index.js │ │ │ ├── library.js │ │ │ ├── microfrontend.js │ │ │ └── module.js │ │ ├── publish │ │ │ ├── index.js │ │ │ ├── publish-github.js │ │ │ └── publish.ts │ │ ├── start │ │ │ ├── all.js │ │ │ ├── index.js │ │ │ ├── proxy-server.js │ │ │ ├── proxy.js │ │ │ └── single.js │ │ └── utils │ │ │ ├── config.js │ │ │ ├── create-sw.js │ │ │ ├── env.js │ │ │ ├── fs.js │ │ │ ├── log.js │ │ │ ├── paths.js │ │ │ └── process.js │ ├── templates │ │ ├── app │ │ │ └── .gitignore │ │ ├── library │ │ │ └── src │ │ │ │ ├── index.js │ │ │ │ └── lib │ │ │ │ └── schema.js │ │ ├── microfrontend-library │ │ │ └── src │ │ │ │ ├── index.js │ │ │ │ └── lib │ │ │ │ └── schema.js │ │ └── webapp │ │ │ └── src │ │ │ ├── App.css │ │ │ ├── App.js │ │ │ └── index.js │ ├── test │ │ ├── __snapshots__ │ │ │ └── dist │ │ │ │ ├── CREATE_APP │ │ │ │ ├── .gitignore │ │ │ │ ├── package.json │ │ │ │ ├── packages │ │ │ │ │ └── webapp │ │ │ │ │ │ ├── .gitignore │ │ │ │ │ │ ├── README.md │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ ├── public │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── logo192.png │ │ │ │ │ │ ├── logo512.png │ │ │ │ │ │ ├── manifest.json │ │ │ │ │ │ └── robots.txt │ │ │ │ │ │ ├── src │ │ │ │ │ │ ├── App.css │ │ │ │ │ │ ├── App.js │ │ │ │ │ │ ├── App.test.js │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── logo.svg │ │ │ │ │ │ ├── serviceWorker.js │ │ │ │ │ │ └── setupTests.js │ │ │ │ │ │ └── yarn.lock │ │ │ │ └── yarn.lock │ │ │ │ └── CREATE_APP_WITH_MICRO │ │ │ │ ├── .gitignore │ │ │ │ ├── package.json │ │ │ │ ├── packages │ │ │ │ ├── microfrontend │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── README.md │ │ │ │ │ ├── package.json │ │ │ │ │ ├── public │ │ │ │ │ │ ├── favicon.ico │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ ├── logo192.png │ │ │ │ │ │ ├── logo512.png │ │ │ │ │ │ ├── manifest.json │ │ │ │ │ │ └── robots.txt │ │ │ │ │ ├── src │ │ │ │ │ │ ├── App.css │ │ │ │ │ │ ├── App.js │ │ │ │ │ │ ├── App.test.js │ │ │ │ │ │ ├── index.css │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── logo.svg │ │ │ │ │ │ ├── serviceWorker.js │ │ │ │ │ │ └── setupTests.js │ │ │ │ │ └── yarn.lock │ │ │ │ └── webapp │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── README.md │ │ │ │ │ ├── package.json │ │ │ │ │ ├── public │ │ │ │ │ ├── favicon.ico │ │ │ │ │ ├── index.html │ │ │ │ │ ├── logo192.png │ │ │ │ │ ├── logo512.png │ │ │ │ │ ├── manifest.json │ │ │ │ │ └── robots.txt │ │ │ │ │ ├── src │ │ │ │ │ ├── App.css │ │ │ │ │ ├── App.js │ │ │ │ │ ├── App.test.js │ │ │ │ │ ├── index.css │ │ │ │ │ ├── index.js │ │ │ │ │ ├── logo.svg │ │ │ │ │ ├── serviceWorker.js │ │ │ │ │ └── setupTests.js │ │ │ │ │ └── yarn.lock │ │ │ │ └── yarn.lock │ │ └── create-app.test.js │ └── yarn.lock ├── doc │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs │ │ ├── backoffice.mdx │ │ ├── backoffice │ │ │ ├── developing.md │ │ │ ├── namespace.md │ │ │ ├── organizing-microfrontends.md │ │ │ ├── release.md │ │ │ ├── roadmap.md │ │ │ ├── setup-application.md │ │ │ └── setup-environment.md │ │ ├── cli.mdx │ │ ├── cli │ │ │ ├── build.md │ │ │ ├── create.md │ │ │ ├── future.md │ │ │ ├── publish.md │ │ │ └── start.md │ │ ├── components │ │ │ └── tag.jsx │ │ ├── core.mdx │ │ ├── core │ │ │ ├── examples.md │ │ │ └── microfrontend.md │ │ ├── development.mdx │ │ ├── development │ │ │ └── build-and-deploy.md │ │ ├── getting-started.mdx │ │ ├── how-it-works.mdx │ │ └── support.md │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── src │ │ ├── css │ │ │ └── custom.css │ │ └── pages │ │ │ ├── index.js │ │ │ ├── logo.svg │ │ │ └── styles.module.css │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ ├── favicon.ico │ │ │ ├── ilustration │ │ │ ├── undraw_apps.svg │ │ │ ├── undraw_coding.svg │ │ │ ├── undraw_cup_of_tea.svg │ │ │ ├── undraw_dev_productivity.svg │ │ │ ├── undraw_divide.svg │ │ │ ├── undraw_feedback.svg │ │ │ ├── undraw_placeholders.svg │ │ │ ├── undraw_react.svg │ │ │ ├── undraw_shipping.svg │ │ │ ├── undraw_spread_love.svg │ │ │ ├── undraw_under_construction.svg │ │ │ ├── undraw_validation.svg │ │ │ └── undraw_works.svg │ │ │ ├── logo-dark.svg │ │ │ ├── logo-white.svg │ │ │ └── logo.svg │ └── yarn.lock ├── e2e │ ├── .gitignore │ ├── cypress.json │ ├── cypress │ │ ├── integration │ │ │ └── checkup.spec.js │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── commands.js │ │ │ └── index.js │ ├── package.json │ ├── src │ │ └── run.js │ └── yarn.lock ├── react │ ├── .babelrc │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── api │ │ │ ├── index.ts │ │ │ ├── schema │ │ │ │ ├── function.ts │ │ │ │ ├── meta.ts │ │ │ │ ├── property.ts │ │ │ │ └── topic.ts │ │ │ ├── shared.ts │ │ │ ├── state │ │ │ │ ├── connector.tsx │ │ │ │ ├── provider.tsx │ │ │ │ └── redux.tsx │ │ │ └── test │ │ │ │ ├── createlib.test.ts │ │ │ │ └── state.test.tsx │ │ ├── base │ │ │ ├── communication.ts │ │ │ └── fetch-retry.ts │ │ ├── container │ │ │ ├── communication.ts │ │ │ ├── context │ │ │ │ ├── consumer.tsx │ │ │ │ ├── index.ts │ │ │ │ └── provider.ts │ │ │ ├── controller │ │ │ │ ├── index.ts │ │ │ │ └── microfrontend.ts │ │ │ └── index.tsx │ │ ├── index.tsx │ │ ├── microfrontend │ │ │ ├── communication.tsx │ │ │ └── index.tsx │ │ ├── react-app-env.d.ts │ │ ├── renderer │ │ │ ├── index.tsx │ │ │ ├── route.tsx │ │ │ └── type.ts │ │ ├── setupTests.ts │ │ └── test │ │ │ ├── container.test.tsx │ │ │ └── mock │ │ │ ├── microfrontend │ │ │ ├── index.tsx │ │ │ ├── lib.ts │ │ │ └── schema.ts │ │ │ └── webapp │ │ │ └── index.tsx │ ├── tsconfig.json │ └── yarn.lock ├── server │ ├── .gcloudignore │ ├── .gitignore │ ├── app.yaml │ ├── cases.md │ ├── decs.d.ts │ ├── docker-compose.yml │ ├── lib │ │ └── index.js │ ├── migration │ │ ├── 1596139327650-Initial.ts │ │ └── 1598386312520-ProjectLink.ts │ ├── package.json │ ├── src │ │ ├── account │ │ │ ├── controller.ts │ │ │ ├── filter.ts │ │ │ ├── firebase-wrapper.ts │ │ │ ├── router.ts │ │ │ └── user.ts │ │ ├── application │ │ │ ├── controller.ts │ │ │ └── router.ts │ │ ├── base │ │ │ ├── controller.ts │ │ │ ├── error-filter.ts │ │ │ ├── errors │ │ │ │ ├── forbidden.ts │ │ │ │ ├── not-found.ts │ │ │ │ └── request-error.ts │ │ │ └── router.ts │ │ ├── dashboard │ │ │ ├── controller.ts │ │ │ └── router.ts │ │ ├── database.ts │ │ ├── entity │ │ │ ├── application.ts │ │ │ ├── deploy.ts │ │ │ ├── deploy │ │ │ │ ├── application-deploy.ts │ │ │ │ ├── microfrontend-deploy.ts │ │ │ │ ├── namespace-deploy.ts │ │ │ │ └── path │ │ │ │ │ └── index.ts │ │ │ ├── destination.ts │ │ │ ├── integration │ │ │ │ ├── aws-s3.ts │ │ │ │ ├── base.ts │ │ │ │ ├── github.ts │ │ │ │ └── types.ts │ │ │ ├── microfrontend.ts │ │ │ ├── namespace.ts │ │ │ ├── user.ts │ │ │ └── version.ts │ │ ├── external │ │ │ ├── list-folder.ts │ │ │ ├── path │ │ │ │ ├── index.ts │ │ │ │ └── path.test.ts │ │ │ └── utils │ │ │ │ └── fs.ts │ │ ├── github │ │ │ ├── client.ts │ │ │ └── octokat.ts │ │ ├── index.ts │ │ ├── integration │ │ │ ├── controller.ts │ │ │ └── router.ts │ │ ├── microfrontend │ │ │ ├── controller.ts │ │ │ └── router.ts │ │ ├── namespace │ │ │ ├── controller.ts │ │ │ └── router.ts │ │ ├── notification │ │ │ ├── integrations │ │ │ │ └── slack.ts │ │ │ └── notification.ts │ │ ├── server.ts │ │ └── version │ │ │ ├── controller.ts │ │ │ └── router.ts │ ├── tsconfig.json │ └── yarn.lock └── webapp │ ├── .gitignore │ ├── lib │ └── index.js │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo.png │ ├── logo.svg │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── app │ │ ├── home │ │ │ ├── App.css │ │ │ ├── dashboards.tsx │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── router.tsx │ ├── assets │ │ ├── github.svg │ │ └── logo.svg │ ├── base │ │ ├── components │ │ │ ├── page │ │ │ │ ├── index.tsx │ │ │ │ └── page.css │ │ │ └── section │ │ │ │ └── index.tsx │ │ └── hooks │ │ │ ├── api-action.ts │ │ │ ├── local-storage.ts │ │ │ ├── logged-user.ts │ │ │ ├── query-param.ts │ │ │ └── request.ts │ ├── index.tsx │ ├── modules │ │ ├── account │ │ │ ├── firebase.ts │ │ │ ├── login.css │ │ │ ├── login.tsx │ │ │ └── profile.tsx │ │ ├── application │ │ │ ├── details │ │ │ │ ├── index.tsx │ │ │ │ ├── microfrontend-list.tsx │ │ │ │ └── namespace-list.tsx │ │ │ ├── fetch.tsx │ │ │ ├── index.tsx │ │ │ ├── list.tsx │ │ │ └── new.tsx │ │ ├── github │ │ │ ├── import.tsx │ │ │ ├── index.tsx │ │ │ └── repositories.tsx │ │ ├── microfrontend │ │ │ ├── details.tsx │ │ │ ├── form.tsx │ │ │ ├── index.tsx │ │ │ ├── list.tsx │ │ │ └── new.tsx │ │ ├── namespace │ │ │ ├── deploy.tsx │ │ │ ├── details │ │ │ │ ├── deploy-list.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── namespace-list.tsx │ │ │ ├── fetch.tsx │ │ │ └── index.tsx │ │ └── version │ │ │ ├── details.tsx │ │ │ ├── form.tsx │ │ │ ├── index.tsx │ │ │ └── new.tsx │ ├── react-app-env.d.ts │ └── setupTests.ts │ ├── tsconfig.json │ └── yarn.lock ├── presentation_guide.md ├── scripts ├── publish-site.js ├── recreate-lock.sh ├── start.js └── utils.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | extends: 'airbnb', 5 | env: { 6 | "jest": true 7 | }, 8 | settings: { 9 | 'import/resolver': { 10 | 'eslint-import-resolver-lerna': { 11 | packages: path.resolve(__dirname, 'packages') 12 | } 13 | } 14 | }, 15 | rules: { 16 | ['arrow-parens']: [2, "as-needed", { "requireForBlockBody": true }] 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /builds 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | lerna-debug.log* 26 | -------------------------------------------------------------------------------- /.imgbotconfig: -------------------------------------------------------------------------------- 1 | "ignoredFiles": [ 2 | "**/__snapshots__/*" 3 | ] 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "printWidth": 120, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12.13' 4 | cache: 5 | yarn: true 6 | directories: 7 | - "node_modules" 8 | before_script: 9 | - yarn lerna-bootstrap 10 | script: 11 | - yarn test 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[javascript]": { 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | }, 6 | "folders": [], 7 | "settings": {}, 8 | "editor.formatOnSave": true, 9 | "editor.useTabStops": false, 10 | "prettier.printWidth": 120, 11 | "prettier.singleQuote": true, 12 | "editor.detectIndentation": false, 13 | "editor.tabSize": 2, 14 | "workbench.colorCustomizations": { 15 | "activityBar.activeBackground": "#ab307e", 16 | "activityBar.activeBorder": "#25320e", 17 | "activityBar.background": "#ab307e", 18 | "activityBar.foreground": "#e7e7e7", 19 | "activityBar.inactiveForeground": "#e7e7e799", 20 | "activityBarBadge.background": "#25320e", 21 | "activityBarBadge.foreground": "#e7e7e7", 22 | "statusBar.background": "#832561", 23 | "statusBar.foreground": "#e7e7e7", 24 | "statusBarItem.hoverBackground": "#ab307e", 25 | "titleBar.activeBackground": "#832561", 26 | "titleBar.activeForeground": "#e7e7e7", 27 | "titleBar.inactiveBackground": "#83256199", 28 | "titleBar.inactiveForeground": "#e7e7e799" 29 | }, 30 | "peacock.color": "#832561" 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Create Micro React App 2 | 3 | This is a WIP file. 4 | 5 | If you want to contribute now, please reach out! Create an issue and lets discuss about it! (: 6 | 7 | ## Backoffice 8 | 9 | Checkout our [doc](https://matheusmr13.github.io/create-micro-react-app/docs/backoffice/developing). 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matheus Martins 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 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Node.js with React 2 | # Build a Node.js project that uses React. 3 | # Add steps that analyze code, save build artifacts, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/javascript 5 | trigger: 6 | - master 7 | pool: 8 | vmImage: 'ubuntu-latest' 9 | steps: 10 | - task: NodeTool@0 11 | inputs: 12 | versionSpec: '12.13' 13 | displayName: 'Install Node.js' 14 | - script: | 15 | yarn 16 | yarn lerna-bootstrap 17 | yarn test 18 | # workingDirectory: ./packages/react 19 | displayName: 'npm install and test' 20 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/icon.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*"], 3 | "version": "independent", 4 | "npmClient": "yarn" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "dotenv": "^8.2.0", 6 | "eslint": "^6.1.0", 7 | "eslint-config-airbnb": "^18.0.1", 8 | "eslint-plugin-import": "^2.19.1", 9 | "eslint-plugin-jsx-a11y": "^6.2.3", 10 | "eslint-plugin-react": "^7.17.0", 11 | "eslint-plugin-react-hooks": "^1.7.0", 12 | "lerna": "^3.19.0" 13 | }, 14 | "scripts": { 15 | "test": "lerna run --stream --concurrency=1 test", 16 | "postinstall": "yarn lerna-bootstrap", 17 | "lerna-bootstrap": "lerna bootstrap", 18 | "start:backoffice": "./scripts/start.js", 19 | "deploy:docs": "./scripts/publish-site.js" 20 | }, 21 | "dependencies": { 22 | "eslint-import-resolver-lerna": "^1.1.0", 23 | "gh-pages": "^2.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/all/.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | node_modules -------------------------------------------------------------------------------- /packages/all/index.js: -------------------------------------------------------------------------------- 1 | const NodeApp = require('@cmra/server'); 2 | const Webapp = require('@cmra/webapp'); 3 | 4 | const configJson = require('./config.json'); 5 | /* 6 | { 7 | "firebase": { 8 | ...config from firebase console... 9 | }, 10 | "firebaseAdmin": { 11 | ...config from google cloud api console... 12 | }, 13 | "database": { 14 | "host": "...", 15 | "port": "...", 16 | "username": "...", 17 | "password": "...", 18 | "database": "..." 19 | }, 20 | "baseUrl": "http://localhost:8080/" 21 | } 22 | */ 23 | 24 | if (!configJson) throw new Error('No config.json found'); 25 | 26 | const run = async () => { 27 | const destFolder = await Webapp.build({ 28 | env: { 29 | FIREBASE_CONFIG_JSON: JSON.stringify(configJson.firebase), 30 | BASE_URL: configJson.baseUrl, 31 | }, 32 | }); 33 | NodeApp.withDatabase(configJson.database) 34 | .withFirebaseConfig(configJson.firebaseAdmin) 35 | .withStaticFiles(destFolder) 36 | .run(8080); 37 | }; 38 | 39 | run(); 40 | -------------------------------------------------------------------------------- /packages/all/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "all", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@cmra/server": "*", 8 | "@cmra/webapp": "*" 9 | }, 10 | "scripts": { 11 | "start": "node index.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/__snapshots__/dist-new 3 | -------------------------------------------------------------------------------- /packages/cli/config/cra-webpack-config-override.js: -------------------------------------------------------------------------------- 1 | const { override, overrideDevServer } = require('customize-cra'); 2 | const { microfrontendFolderName } = require('../scripts/utils/config'); 3 | const { appPackageJson } = require('../scripts/utils/paths'); 4 | const { escapePackageName } = require('../scripts/utils/paths'); 5 | 6 | // eslint-disable-next-line 7 | const packageJson = require(appPackageJson); 8 | 9 | const overrideWebpackConfigs = () => (config) => { 10 | const newConfig = { ...config }; 11 | const escapedPackageName = escapePackageName(packageJson.name); 12 | newConfig.output.jsonpFunction = escapedPackageName; 13 | 14 | if (process.env.NODE_ENV === 'production') { 15 | if (process.env.IS_MICROFRONTEND) { 16 | newConfig.output.publicPath = `./${microfrontendFolderName}/${escapedPackageName}/`; 17 | } else { 18 | newConfig.output.publicPath = './'; 19 | } 20 | } else if (process.env.IS_MICROFRONTEND) { 21 | newConfig.output.publicPath = `http://localhost:${process.env.PORT}/`; 22 | } 23 | 24 | return newConfig; 25 | }; 26 | 27 | const overrideDevServerConfigs = () => (config) => { 28 | const newConfig = { ...config }; 29 | if (process.env.IS_MICROFRONTEND) { 30 | newConfig.headers = { 31 | 'Access-Control-Allow-Origin': 'http://localhost:3000', 32 | 'Access-Control-Allow-Credentials': 'true', 33 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-id, Content-Length, X-Requested-With', 34 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 35 | }; 36 | } 37 | return newConfig; 38 | }; 39 | 40 | module.exports = { 41 | webpack: override(overrideWebpackConfigs()), 42 | devServer: overrideDevServer(overrideDevServerConfigs()), 43 | }; 44 | -------------------------------------------------------------------------------- /packages/cli/config/project-configs.js: -------------------------------------------------------------------------------- 1 | const { getAppFile } = require('../utils/fs'); 2 | 3 | const buildAllConfigurationsFile = getAppFile('build-configuration.js'); 4 | const buildAllConfigurations = buildAllConfigurationsFile(); 5 | 6 | const defaults = { 7 | shouldBuildPackages: false, 8 | app: 'webapp', 9 | packagesFolder: 'packages', 10 | microfrontendFolderName: 'microfrontends', 11 | allBuildsFolder: 'builds', 12 | distFolder: 'build', 13 | }; 14 | 15 | 16 | module.exports = { ...defaults, ...buildAllConfigurations }; 17 | -------------------------------------------------------------------------------- /packages/cli/config/sw-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | staticFileGlobs: 3 | [ 4 | './build/**/**.html', 5 | './build/vendor/**', 6 | './build/meta/**', 7 | './build/bundle/**', 8 | './build/static/js/*.js', 9 | './build/static/css/*.css', 10 | './build/static/media/**', 11 | './build/**/vendor/**', 12 | './build/**/meta/**', 13 | './build/**/bundle/**', 14 | './build/**/static/js/*.js', 15 | './build/**/static/css/*.css', 16 | './build/**/static/media/**', 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cmra/cli", 3 | "version": "0.0.4", 4 | "main:src": "src/index.js", 5 | "main": "index.js", 6 | "author": "Matheus Martins", 7 | "license": "MIT", 8 | "bin": { 9 | "cmra": "./bin/index.js" 10 | }, 11 | "optionalDependencies": { 12 | "node-sass": "^4.13.0" 13 | }, 14 | "dependencies": { 15 | "axios": "^0.19.0", 16 | "chalk": "^3.0.0", 17 | "commander": "^4.0.1", 18 | "customize-cra": "^0.8.0", 19 | "express": "^4.17.1", 20 | "express-http-proxy": "^1.6.0", 21 | "gh-pages": "^2.2.0", 22 | "ora": "^4.0.3", 23 | "react-app-rewired": "^2.1.5", 24 | "react-scripts": "^3.2.0", 25 | "semver": "^6.3.0", 26 | "uglify-js": "^3.6.7", 27 | "workbox-build": "^5.1.2" 28 | }, 29 | "scripts": { 30 | "test:watch": "jest watch ./test", 31 | "test": "echo 'PASS'" 32 | }, 33 | "devDependencies": { 34 | "dir-compare": "^2.2.0", 35 | "jest": "^25.2.3", 36 | "rollup-plugin-typescript": "^1.0.1", 37 | "tslib": "^1.10.0", 38 | "typescript": "^3.7.4" 39 | }, 40 | "engines": { 41 | "node": ">=12.10" 42 | }, 43 | "jest": { 44 | "testPathIgnorePatterns": [ 45 | "/test/__snapshots__" 46 | ], 47 | "testTimeout": 10000000 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli/scripts/build/all.js: -------------------------------------------------------------------------------- 1 | const { getPackagesFromConfig } = require('../utils/config'); 2 | const { copyFolder, rm, mkdir } = require('../utils/fs'); 3 | const { escapePackageName } = require('../utils/paths'); 4 | 5 | const { exec } = require('../utils/process'); 6 | 7 | const allBuildsFolder = 'builds'; 8 | 9 | const buildPackage = async (packageToBuild, isMicro) => 10 | exec(`npm run --prefix ${packageToBuild} build`); 11 | 12 | const buildAll = async (opts) => { 13 | const { 14 | microfrontends, 15 | app, 16 | } = await getPackagesFromConfig(opts.configurationFile, opts); 17 | 18 | await rm(allBuildsFolder); 19 | await mkdir(allBuildsFolder); 20 | 21 | const allPackages = { ...microfrontends, ...app }; 22 | const appName = Object.keys(app)[0]; 23 | 24 | await Promise.all(Object.keys(allPackages).map(async (packageToBuild) => { 25 | const pathToBuild = allPackages[packageToBuild]; 26 | 27 | const escapedPackageName = escapePackageName(packageToBuild); 28 | await buildPackage(pathToBuild, packageToBuild === appName); 29 | await copyFolder(`${pathToBuild}/build`, `./${allBuildsFolder}/${escapedPackageName}`); 30 | })); 31 | }; 32 | 33 | module.exports = buildAll; 34 | -------------------------------------------------------------------------------- /packages/cli/scripts/build/index.js: -------------------------------------------------------------------------------- 1 | const buildAll = require('./all'); 2 | const packageAll = require('./package'); 3 | const buildSingle = require('./single'); 4 | const buildLibrary = require('./library'); 5 | 6 | const TYPE = { 7 | SINGLE: 'SINGLE', 8 | ALL: 'ALL', 9 | LIBRARY: 'LIBRARY', 10 | PACKAGE: 'PACKAGE', 11 | }; 12 | 13 | const start = (type, opts) => { 14 | ({ 15 | [TYPE.SINGLE]: () => { 16 | const { shouldBuildStandalone, pathToSchema } = opts; 17 | buildSingle(shouldBuildStandalone, pathToSchema); 18 | }, 19 | [TYPE.ALL]: () => buildAll(opts), 20 | [TYPE.PACKAGE]: () => packageAll(opts), 21 | [TYPE.LIBRARY]: () => buildLibrary(opts), 22 | }[type]()); 23 | }; 24 | 25 | start.TYPE = TYPE; 26 | 27 | module.exports = start; 28 | -------------------------------------------------------------------------------- /packages/cli/scripts/build/library.js: -------------------------------------------------------------------------------- 1 | const { resolveApp, appPackageJson } = require('../utils/paths'); 2 | const { escapePackageName } = require('../utils/paths'); 3 | const { readJson, rm, mkdir, copyFile, writeFile } = require('../utils/fs'); 4 | 5 | const getExtension = (file) => { 6 | const parts = file.split('.'); 7 | return parts[parts.length - 1]; 8 | }; 9 | 10 | const build = async ({ pathToSchema }) => { 11 | const buildLibFolder = resolveApp('build-lib'); 12 | const packageJson = await readJson(appPackageJson); 13 | // const escapedPackageName = escapePackageName(packageJson.name); 14 | 15 | await rm(buildLibFolder); 16 | await mkdir(buildLibFolder); 17 | 18 | await copyFile(resolveApp(pathToSchema), `${buildLibFolder}/lib.${getExtension(pathToSchema)}`); 19 | 20 | await writeFile( 21 | `${buildLibFolder}/index.js`, 22 | ` 23 | import { Api } from './lib'; 24 | export default lib.private(); 25 | ` 26 | ); 27 | }; 28 | 29 | module.exports = build; 30 | -------------------------------------------------------------------------------- /packages/cli/scripts/build/library/library.js: -------------------------------------------------------------------------------- 1 | const { resolveApp, appPackageJson } = require('../../utils/paths'); 2 | const { escapePackageName } = require('../../utils/paths'); 3 | const { readJson, rm, mkdir, copyFile, writeFile } = require('../../utils/fs'); 4 | 5 | const getExtension = (file) => { 6 | const parts = file.split('.'); 7 | return parts[parts.length - 1]; 8 | }; 9 | 10 | const build = async (fileToBuild) => { 11 | const buildLibFolder = resolveApp('build-lib'); 12 | const packageJson = await readJson(appPackageJson); 13 | const escapedPackageName = escapePackageName(packageJson.name); 14 | 15 | await rm(buildLibFolder); 16 | await mkdir(buildLibFolder); 17 | 18 | await copyFile(resolveApp(fileToBuild), `${buildLibFolder}/schema.${getExtension(fileToBuild)}`); 19 | 20 | await writeFile( 21 | `${buildLibFolder}/index.js`, 22 | ` 23 | import { CreateLib } from '@cmra/react'; 24 | import schema from './schema'; 25 | 26 | export default CreateLib(schema, { 27 | apiAccess: CreateLib.BUILD_TYPE.PUBLIC_API, 28 | packageName: "${escapedPackageName}" 29 | }); 30 | ` 31 | ); 32 | }; 33 | 34 | module.exports = build; 35 | -------------------------------------------------------------------------------- /packages/cli/scripts/build/single.js: -------------------------------------------------------------------------------- 1 | const { readJson, getReactAppRewiredPath, writeJson } = require('../utils/fs'); 2 | const { exec } = require('../utils/process'); 3 | const { appPackageJson } = require('../utils/paths'); 4 | const { getEnvString } = require('../utils/env'); 5 | 6 | const buildLibrary = require('./library'); 7 | 8 | const build = async (shouldBuildStandalone, pathToSchema) => { 9 | const packageJson = await readJson(appPackageJson); 10 | 11 | const env = getEnvString({ 12 | packageJson, 13 | isMicrofrontend: !shouldBuildStandalone, 14 | }); 15 | const reactAppRewiredPath = await getReactAppRewiredPath(); 16 | 17 | await exec(`${env} ${reactAppRewiredPath} build --config-overrides ${__dirname}/../../config/cra-webpack-config-override.js`); 18 | await writeJson('build/deps.json', packageJson.dependencies); 19 | 20 | if (pathToSchema) { 21 | await buildLibrary(pathToSchema); 22 | } 23 | }; 24 | 25 | module.exports = build; 26 | -------------------------------------------------------------------------------- /packages/cli/scripts/create/app.js: -------------------------------------------------------------------------------- 1 | const { createModule, addScriptsToPackageJson } = require('./module'); 2 | 3 | const { explain } = require('../utils/log'); 4 | const { mkdir, copyTemplateTo } = require('../utils/fs'); 5 | const { resolveApp } = require('../utils/paths'); 6 | const { createExecutionContext } = require('../utils/process'); 7 | 8 | const rootAppScripts = (webappName) => ({ 9 | postinstall: 'lerna bootstrap', 10 | build: 'yarn build:packages && yarn package', 11 | 'build:packages': `cmra build -a ${webappName}`, 12 | package: `cmra build -p ${webappName}`, 13 | start: `cmra start -a ${webappName}`, 14 | }); 15 | 16 | const createApp = async (name, opts = {}) => { 17 | const { webappName = 'webapp' } = opts; 18 | 19 | const rootAppPath = resolveApp(name); 20 | 21 | const { execInRoot } = createExecutionContext(rootAppPath, webappName); 22 | 23 | const configureRootApp = async () => { 24 | await execInRoot('yarn init --yes'); 25 | await copyTemplateTo('app', rootAppPath); 26 | 27 | await execInRoot('yarn add lerna@"<4.0.0"'); 28 | await execInRoot('yarn add @cmra/cli'); 29 | 30 | await addScriptsToPackageJson(`${rootAppPath}/package.json`, rootAppScripts(webappName)); 31 | }; 32 | 33 | await explain('Creating folder', () => mkdir(`${name}/packages`)); 34 | 35 | await explain('Configuring root app and webapp', () => 36 | Promise.all([configureRootApp(), createModule(webappName, 'webapp', rootAppPath)]) 37 | ); 38 | }; 39 | 40 | module.exports = createApp; 41 | -------------------------------------------------------------------------------- /packages/cli/scripts/create/index.js: -------------------------------------------------------------------------------- 1 | const { createMicrofrontend, createMicrofrontendWithLibrary } = require('./microfrontend'); 2 | const createApp = require('./app'); 3 | const { createStandaloneLibrary } = require('./library'); 4 | 5 | const TYPE = { 6 | APP: 'APP', 7 | WEBAPP: 'WEBAPP', 8 | MICROFRONTEND: 'MICROFRONTEND', 9 | LIBRARY: 'LIBRARY', 10 | }; 11 | 12 | const create = async (types, name, opts) => { 13 | const { 14 | pathToCreate = '.', 15 | template, // TODO: implement this with create react app 16 | } = opts; 17 | 18 | const hasType = (type) => types.indexOf(type) > -1; 19 | 20 | if (hasType(create.TYPE.WEBAPP)) throw new Error('Webapp creation alone not implemented yet.'); 21 | 22 | const shouldCreateApp = hasType(create.TYPE.APP); 23 | const shouldCreateMicro = hasType(create.TYPE.MICROFRONTEND) || hasType(create.TYPE.APP); 24 | const shouldCreateLib = hasType(create.TYPE.LIBRARY); 25 | 26 | if (shouldCreateApp) { 27 | await createApp(name); 28 | } 29 | 30 | if (shouldCreateMicro) { 31 | const microfrontendName = shouldCreateApp ? 'microfrontend' : name; 32 | const pathToCreateMicro = shouldCreateApp ? name : pathToCreate; 33 | if (shouldCreateLib) { 34 | await createMicrofrontendWithLibrary(microfrontendName, pathToCreateMicro); 35 | } else { 36 | await createMicrofrontend(microfrontendName, pathToCreateMicro, !shouldCreateApp); 37 | } 38 | return; 39 | } 40 | 41 | if (shouldCreateLib) { 42 | const libName = shouldCreateApp ? 'library' : name; 43 | const pathToCreateLib = shouldCreateApp ? name : pathToCreate; 44 | await createStandaloneLibrary(libName, pathToCreateLib); 45 | } 46 | }; 47 | 48 | create.TYPE = TYPE; 49 | 50 | module.exports = create; 51 | -------------------------------------------------------------------------------- /packages/cli/scripts/create/microfrontend.js: -------------------------------------------------------------------------------- 1 | const { createModule } = require('./module'); 2 | const { createLibrary } = require('./library'); 3 | 4 | const { writeFile } = require('../utils/fs'); 5 | const { explain } = require('../utils/log'); 6 | const { resolveApp, resolvePackageSrc } = require('../utils/paths'); 7 | 8 | const indexJsFile = (packageName) => ` 9 | import { ExportMicrofrontend } from '@cmra/react'; 10 | import App from './App'; 11 | 12 | ExportMicrofrontend({ 13 | name: '${packageName}', 14 | view: App, 15 | }); 16 | `; 17 | 18 | const createMicrofrontendWithTemplate = async (name, folder, isRootPath) => { 19 | await createModule(name, 'microfrontend', resolveApp(folder), isRootPath); 20 | if (isRootPath) { 21 | await writeFile(resolveApp(folder, name, 'src', 'index.js'), indexJsFile(name)); 22 | } else { 23 | await writeFile(resolvePackageSrc(folder, name, 'index.js'), indexJsFile(name)); 24 | } 25 | }; 26 | 27 | const createMicrofrontend = async (name, folder = '.', isRootPath) => { 28 | await explain('Creating microfrontend', () => createMicrofrontendWithTemplate(name, folder, isRootPath)); 29 | }; 30 | 31 | const createMicrofrontendWithLibrary = async (name, folder = '.') => { 32 | await explain('Creating microfrontend with library', () => createLibrary(name, folder, 'microfrontend-library')); 33 | }; 34 | 35 | module.exports = { 36 | createMicrofrontend, 37 | createMicrofrontendWithLibrary, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/cli/scripts/publish/index.js: -------------------------------------------------------------------------------- 1 | const publishGithub = require('./publish-github'); 2 | 3 | const publish = () => { 4 | publishGithub(); 5 | }; 6 | 7 | module.exports = publish; 8 | -------------------------------------------------------------------------------- /packages/cli/scripts/publish/publish-github.js: -------------------------------------------------------------------------------- 1 | const ghPages = require('gh-pages'); 2 | const { appPackageJson } = require('../utils/paths'); 3 | const { readJson } = require('../utils/fs'); 4 | const { explain } = require('../utils/log'); 5 | 6 | const publish = async () => { 7 | const packageJson = await readJson(appPackageJson); 8 | 9 | const escapePackageName = (packageName) => packageName.replace(/@/g, '').replace(/\//g, '_'); 10 | 11 | const dest = `versions/${escapePackageName(packageJson.name)}/${packageJson.version}`; 12 | await new Promise((resolve, reject) => { 13 | ghPages.publish( 14 | 'build', 15 | { 16 | dest, 17 | branch: 'versions', 18 | }, 19 | (error) => { 20 | if (error) { 21 | console.error(error); 22 | reject(error); 23 | return; 24 | } 25 | resolve(); 26 | } 27 | ); 28 | }); 29 | }; 30 | 31 | module.exports = () => explain('Publishing build folder to github versions branch', publish); 32 | -------------------------------------------------------------------------------- /packages/cli/scripts/start/all.js: -------------------------------------------------------------------------------- 1 | const { microfrontendFolderName, getPackagesFromConfig } = require('../utils/config'); 2 | const { writeJson } = require('../utils/fs'); 3 | const { escapePackageName } = require('../utils/paths'); 4 | const startSingleApp = require('./single'); 5 | const startProxyServer = require('./proxy-server'); 6 | const { getMetaFromUrl } = require('./proxy'); 7 | 8 | const startMultipleLocations = async (configurationFilePath, opts = {}) => { 9 | const { microfrontends, app, proxyUrl } = await getPackagesFromConfig(configurationFilePath, opts); 10 | 11 | const INITIAL_PORT = 3001; 12 | 13 | const metaJson = Object.keys(microfrontends).reduce( 14 | (agg, packageName, i) => 15 | Object.assign(agg, { 16 | [escapePackageName(packageName)]: { 17 | host: `http://localhost:${INITIAL_PORT + i}`, 18 | }, 19 | }), 20 | {} 21 | ); 22 | 23 | const pathToAppPackage = Object.values(app)[0]; 24 | 25 | const envJson = await getMetaFromUrl(proxyUrl); 26 | await writeJson(`${pathToAppPackage}/public/${microfrontendFolderName}/meta.json`, { 27 | ...envJson, 28 | ...metaJson, 29 | }); 30 | 31 | Object.values(microfrontends).forEach((packagePath, i) => 32 | startSingleApp({ 33 | pathToPackage: packagePath, 34 | port: INITIAL_PORT + i, 35 | isMicro: true, 36 | isRunningAll: true, 37 | }) 38 | ); 39 | 40 | startSingleApp({ 41 | pathToPackage: pathToAppPackage, 42 | port: 3000, 43 | isRunningAll: true, 44 | }); 45 | }; 46 | 47 | module.exports = startMultipleLocations; 48 | -------------------------------------------------------------------------------- /packages/cli/scripts/start/index.js: -------------------------------------------------------------------------------- 1 | const startProxyServer = require('./proxy-server'); 2 | const startLocalAll = require('./all'); 3 | const startSingle = require('./single'); 4 | const { writeJson } = require('../utils/fs'); 5 | const { resolveApp } = require('../utils/paths'); 6 | const { getMetaFromUrl } = require('./proxy'); 7 | 8 | const TYPE = { 9 | SINGLE: 'SINGLE', 10 | LOCAL: 'LOCAL', 11 | PROXY: 'PROXY', 12 | }; 13 | 14 | const start = (type, opts) => { 15 | ({ 16 | [TYPE.SINGLE]: startSingle, 17 | [TYPE.LOCAL]: () => { 18 | const { configurationFile } = opts; 19 | startLocalAll(configurationFile, opts); 20 | }, 21 | [TYPE.PROXY]: async () => { 22 | const { url, isContainer } = opts; 23 | if (!isContainer) { 24 | startProxyServer(url); 25 | } else { 26 | const envJson = await getMetaFromUrl(url); 27 | await writeJson(resolveApp('public/microfrontends/meta.json'), envJson); 28 | } 29 | startSingle({ port: isContainer ? 3000 : 3001, isMicro: !isContainer }); 30 | }, 31 | }[type]()); 32 | }; 33 | 34 | start.TYPE = TYPE; 35 | 36 | module.exports = start; 37 | -------------------------------------------------------------------------------- /packages/cli/scripts/start/proxy-server.js: -------------------------------------------------------------------------------- 1 | const proxy = require('express-http-proxy'); 2 | const express = require('express'); 3 | const axios = require('axios'); 4 | const { readJson } = require('../utils/fs'); 5 | const { escapePackageName, appPackageJson } = require('../utils/paths'); 6 | 7 | const startProxyServer = async (proxyUrl, opts = {}) => { 8 | const packageJson = await readJson(appPackageJson); 9 | const app = express(); 10 | const PORT = 3000; 11 | const escapedPackageName = escapePackageName(packageJson.name); 12 | 13 | const url = new URL(proxyUrl); 14 | const namespace = url.pathname; 15 | 16 | app.get(`${namespace}microfrontends/meta.json`, (_, res) => { 17 | axios 18 | .get(`${proxyUrl}microfrontends/meta.json`) 19 | .then((response) => response.data) 20 | .then((json) => { 21 | res.json({ 22 | ...json, 23 | [escapedPackageName]: { host: 'http://localhost:3001' }, 24 | }); 25 | }); 26 | }); 27 | 28 | app.use( 29 | namespace, 30 | proxy(proxyUrl, { 31 | proxyReqPathResolver: (req) => `${namespace}${req.url}`, 32 | }) 33 | ); 34 | 35 | app.use('/', proxy(url.origin)); 36 | 37 | app.listen(PORT, () => 38 | console.log(`Example app listening on PORT ${PORT}! Access localhost:${PORT}${namespace} to see your app`) 39 | ); 40 | }; 41 | 42 | module.exports = startProxyServer; 43 | -------------------------------------------------------------------------------- /packages/cli/scripts/start/proxy.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const getMetaFromUrl = async (url) => { 4 | const envJson = await new Promise((resolve) => { 5 | axios 6 | .get(`${url}microfrontends/meta.json`) 7 | .then((response) => response.data) 8 | .then((json) => { 9 | resolve(json); 10 | }); 11 | }); 12 | Object.values(envJson).forEach((envJson) => { 13 | envJson.js = envJson.js.map((pathToFile) => pathToFile.replace('./', url)); 14 | }); 15 | 16 | return envJson; 17 | }; 18 | 19 | module.exports = { 20 | getMetaFromUrl, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/cli/scripts/start/single.js: -------------------------------------------------------------------------------- 1 | const { getReactAppRewiredPath, readJson } = require('../utils/fs'); 2 | const { exec } = require('../utils/process'); 3 | const { appPackageJson } = require('../utils/paths'); 4 | const { getEnvString } = require('../utils/env'); 5 | 6 | const startSingle = async (opts = {}) => { 7 | const { 8 | isRunningAll, 9 | port, 10 | isMicro, 11 | } = opts; 12 | 13 | let startou = false; 14 | 15 | if (isRunningAll) { 16 | const { 17 | pathToPackage, 18 | } = opts; 19 | const envString = getEnvString({ isMicrofrontend: isMicro, port }); 20 | 21 | await exec(`${envString} npm run --prefix ${pathToPackage} start`, { 22 | onStdout: (data) => { 23 | if (data.toString().indexOf('Starting the development server') > -1) { 24 | console.info(`Startando ${pathToPackage}`); 25 | } else if (data.toString().indexOf('Compiled') > -1) { 26 | console.info(`Startou ${pathToPackage}`); 27 | startou = true; 28 | } 29 | 30 | if (startou) { 31 | console.info(`[LOG FROM ${pathToPackage}]`); 32 | console.info(data.toString()); 33 | } 34 | }, 35 | onStderr: data => process.stderr.write(data), 36 | }); 37 | } else { 38 | const packageJson = await readJson(appPackageJson); 39 | const envString = getEnvString({ packageJson, isMicrofrontend: process.env.IS_MICROFRONTEND || isMicro, port }); 40 | const reactAppRewiredPath = await getReactAppRewiredPath(); 41 | await exec(`${envString} ${reactAppRewiredPath} start --config-overrides ${__dirname}/../../config/cra-webpack-config-override.js`, { 42 | onStdout: data => process.stdout.write(data), 43 | onStderr: data => process.stderr.write(data), 44 | }); 45 | } 46 | }; 47 | 48 | module.exports = startSingle; 49 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/config.js: -------------------------------------------------------------------------------- 1 | const { resolveApp } = require('../utils/paths'); 2 | const { readJson, getDirsFrom } = require('../utils/fs'); 3 | const { escapePackageName } = require('./paths'); 4 | 5 | const defaults = { 6 | microfrontendFolderName: 'microfrontends', 7 | }; 8 | 9 | const getConfigurationBasedOnFolders = async (webappName = 'webapp') => { 10 | const packages = await getDirsFrom('./packages'); 11 | 12 | const microfrontendInfos = await Promise.all(packages 13 | .map(async (dir) => { 14 | const packageJson = await readJson(resolveApp(`${dir}/package.json`)); 15 | const packageName = packageJson.name; 16 | 17 | return { 18 | moduleName: packageName, 19 | path: `./${dir}`, 20 | }; 21 | })); 22 | 23 | let app; 24 | const microfrontends = microfrontendInfos 25 | .reduce((agg, microfrontendInfo) => { 26 | if (microfrontendInfo.moduleName === webappName) { 27 | app = { [microfrontendInfo.moduleName]: microfrontendInfo.path }; 28 | return agg; 29 | } 30 | 31 | return Object.assign(agg, { 32 | [microfrontendInfo.moduleName]: microfrontendInfo.path, 33 | }); 34 | }, {}); 35 | 36 | return ({ 37 | app, 38 | microfrontends, 39 | }); 40 | }; 41 | 42 | const getPackagesFromConfig = async (configurationFilePath, opts) => { 43 | try { 44 | return readJson(resolveApp(configurationFilePath)); 45 | } catch (e) { 46 | console.warn('Configuration file not specified, assuming all microfrontends are located on ./packages'); 47 | return getConfigurationBasedOnFolders(opts.webappName); 48 | } 49 | }; 50 | 51 | module.exports = { 52 | ...defaults, 53 | getPackagesFromConfig, 54 | }; 55 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/create-sw.js: -------------------------------------------------------------------------------- 1 | const workboxBuild = require('workbox-build'); 2 | 3 | module.exports = () => workboxBuild.generateSW({ 4 | swDest: './build/service-worker.js', 5 | globDirectory: './build', 6 | globPatterns: ['**/*.{js,css,html,json}'], 7 | clientsClaim: true, 8 | // exclude: [/\.map$/, /asset-manifest\.json$/], 9 | navigateFallback: '/index.html', 10 | navigateFallbackDenylist: [ 11 | // Exclude URLs starting with /_, as they're likely an API call 12 | new RegExp('^/_'), 13 | // Exclude any URLs whose last part seems to be a file extension 14 | // as they're likely a resource and not a SPA route. 15 | // URLs containing a "?" character won't be blacklisted as they're likely 16 | // a route with query params (e.g. auth callbacks). 17 | new RegExp('/[^/?]+\\.[^/]+$'), 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/env.js: -------------------------------------------------------------------------------- 1 | const { escapePackageName } = require('./paths'); 2 | 3 | const REACT_APP = /^REACT_APP_/i; 4 | 5 | const MICROFRONTEND_ENV = { 6 | BROWSER: 'none', 7 | IS_MICROFRONTEND: true, 8 | }; 9 | 10 | const WEBAPP_ENV = { 11 | }; 12 | 13 | const getEnvString = ({ packageJson, isMicrofrontend, port }) => { 14 | const envs = Object.keys(process.env) 15 | .filter(key => REACT_APP.test(key)) 16 | .reduce((env, key) => Object.assign(env, { [key]: process.env[key] }), { 17 | ...(isMicrofrontend ? MICROFRONTEND_ENV : WEBAPP_ENV), 18 | PORT: port || process.env.PORT || 3000, 19 | ...(packageJson && { REACT_APP_PACKAGE_NAME: escapePackageName(packageJson.name) }), 20 | SKIP_PREFLIGHT_CHECK: true, 21 | }); 22 | 23 | return Object.keys(envs).map(env => `${env}=${envs[env]}`).join(' '); 24 | }; 25 | 26 | module.exports = { 27 | getEnvString, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/log.js: -------------------------------------------------------------------------------- 1 | const ora = require('ora'); 2 | 3 | const explain = async (title, action) => { 4 | const spinner = ora(title).start(); 5 | try { 6 | await action(); 7 | spinner.succeed(); 8 | } catch (e) { 9 | spinner.fail(); 10 | console.error(e); 11 | } 12 | }; 13 | 14 | module.exports = { 15 | explain, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const appDirectory = fs.realpathSync(process.cwd()); 5 | const resolve = (paths) => path.resolve(...[appDirectory].concat(paths)); 6 | const resolveApp = (...relativePath) => resolve(relativePath); 7 | const resolvePackageSrc = (relativePath, packageName, file) => 8 | resolve([relativePath, 'packages', packageName, 'src', file]); 9 | const escapePackageName = (packageName) => packageName.replace(/@/g, '').replace(/\//g, '_'); 10 | 11 | module.exports = { 12 | resolveApp, 13 | resolvePackageSrc, 14 | appPackageJson: resolveApp('package.json'), 15 | escapePackageName, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/cli/scripts/utils/process.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | const exec = (command, { cwd, onStdout, onStderr, debug = true } = {}) => 4 | new Promise((resolve, reject) => { 5 | const spawnProcess = spawn(command, [], { shell: true, cwd }); 6 | 7 | if (onStdout || debug) spawnProcess.stdout.on('data', onStdout || ((data) => process.stdout.write(data))); 8 | if (onStderr || debug) spawnProcess.stderr.on('data', onStderr || ((data) => process.stderr.write(data))); 9 | 10 | spawnProcess.on('exit', (code) => { 11 | if (code > 0) { 12 | reject(code); 13 | return; 14 | } 15 | resolve(); 16 | }); 17 | }); 18 | 19 | const createExecutionContext = (rootAppPath, appName, opts) => { 20 | const execInFolder = (path) => (command) => exec(command, { ...opts, cwd: `${rootAppPath}${path || ''}` }); 21 | return { 22 | execInRoot: execInFolder('/'), 23 | execInRootApp: execInFolder(`/${appName}`), 24 | execInPackages: execInFolder('/packages'), 25 | execInApp: execInFolder(`/packages/${appName}`), 26 | }; 27 | }; 28 | 29 | const rmSync = (path, opts = { recursive: true }) => { 30 | const { recursive } = opts; 31 | exec(`[ -d ${path} ] || [ -f ${path} ] && rm ${recursive ? '-Rf' : ''} ${path}`); 32 | }; 33 | 34 | module.exports = { 35 | exec, 36 | rmSync, 37 | createExecutionContext, 38 | }; 39 | -------------------------------------------------------------------------------- /packages/cli/templates/app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | builds/ 4 | 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | -------------------------------------------------------------------------------- /packages/cli/templates/library/src/index.js: -------------------------------------------------------------------------------- 1 | import { ExportMicrofrontend } from '@cmra/react'; 2 | import schema from './lib/schema'; 3 | 4 | ExportMicrofrontend({ 5 | ...schema, 6 | view: {}, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/cli/templates/library/src/lib/schema.js: -------------------------------------------------------------------------------- 1 | import { CreateLib } from '@cmra/react'; 2 | 3 | export default { 4 | interface: { 5 | examples: { 6 | initialState: null, 7 | }, 8 | addExample: { 9 | type: CreateLib.TYPE.FUNCTION, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/cli/templates/microfrontend-library/src/index.js: -------------------------------------------------------------------------------- 1 | import { ExportMicrofrontend } from '@cmra/react'; 2 | import schema from './lib/schema'; 3 | import App from './App'; 4 | 5 | ExportMicrofrontend({ 6 | ...schema, 7 | view: App, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/cli/templates/microfrontend-library/src/lib/schema.js: -------------------------------------------------------------------------------- 1 | import { CreateLib } from '@cmra/react'; 2 | 3 | export default { 4 | interface: { 5 | examples: { 6 | initialState: null, 7 | }, 8 | addExample: { 9 | type: CreateLib.TYPE.FUNCTION, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/cli/templates/webapp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App__header { 6 | background-color: #282c34; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | font-size: calc(10px + 2vmin); 12 | color: white; 13 | font-weight: bold; 14 | padding: 16px; 15 | line-height: 32px; 16 | margin: 0; 17 | } 18 | 19 | .App__menu { 20 | display: flex; 21 | justify-content: center; 22 | background-color: #575f6d; 23 | } 24 | 25 | .App__menu-item { 26 | border: 0; 27 | background-color: #373f4e; 28 | padding: 16px; 29 | color: white; 30 | text-transform: uppercase; 31 | font-weight: bold; 32 | outline: 0; 33 | cursor: pointer; 34 | } 35 | 36 | .App__menu-item:not(:last-child) { 37 | border-left: #575f6d; 38 | } 39 | 40 | .App__menu-item:hover { 41 | background-color: #2c3340; 42 | } 43 | 44 | .App__menu-item--selected { 45 | background-color: #242b38; 46 | } 47 | 48 | .App__menu-item--selected:hover { 49 | background-color: #242b38; 50 | } 51 | 52 | .App__content { 53 | display: flex; 54 | } 55 | 56 | .App__microfrontend-content { 57 | flex: 1 0 220px; 58 | } -------------------------------------------------------------------------------- /packages/cli/templates/webapp/src/App.js: -------------------------------------------------------------------------------- 1 | import { withMicrofrontend } from '@cmra/react'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | import './App.css'; 6 | 7 | const App = ({ microfrontends }) => { 8 | const [selectedMicrofrontendKey, setSelectedMicrofrontendKey] = useState(undefined); 9 | 10 | const MicrofrontendComponent = selectedMicrofrontendKey 11 | ? microfrontends[selectedMicrofrontendKey] 12 | : Object.values(microfrontends)[0]; 13 | 14 | return ( 15 |
16 |

17 | Welcome to Microfrontend World. 18 |
19 | Choose a microfrontend to show: 20 |

21 |
22 | {Object.keys(microfrontends).map((microfrontend) => ( 23 | 30 | ))} 31 |
32 |
33 | {MicrofrontendComponent && ( 34 |
35 | 36 |
37 | )} 38 |
39 |
40 | ); 41 | }; 42 | export default withMicrofrontend(App); 43 | -------------------------------------------------------------------------------- /packages/cli/templates/webapp/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import { ImportMicrofrontend } from '@cmra/react'; 5 | import App from './App'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | builds/ 4 | 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CREATE_APP", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "lerna": "<4.0.0", 8 | "@cmra/cli": "^2.1.18" 9 | }, 10 | "scripts": { 11 | "postinstall": "lerna bootstrap", 12 | "build": "yarn build:packages && yarn package", 13 | "build:packages": "cmra build -a webapp", 14 | "package": "cmra build -p webapp", 15 | "start": "cmra start -a webapp" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | build-lib 25 | public/meta.json -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@cmra/cli": "^2.1.18", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "@cmra/react": "^2.1.3", 13 | "react-scripts": "3.4.1" 14 | }, 15 | "scripts": { 16 | "start": "cmra start", 17 | "build": "cmra build -s", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/favicon.ico -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/logo192.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/logo512.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App__header { 6 | background-color: #282c34; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | font-size: calc(10px + 2vmin); 12 | color: white; 13 | font-weight: bold; 14 | padding: 16px; 15 | line-height: 32px; 16 | margin: 0; 17 | } 18 | 19 | .App__menu { 20 | display: flex; 21 | justify-content: center; 22 | background-color: #575f6d; 23 | } 24 | 25 | .App__menu-item { 26 | border: 0; 27 | background-color: #373f4e; 28 | padding: 16px; 29 | color: white; 30 | text-transform: uppercase; 31 | font-weight: bold; 32 | outline: 0; 33 | cursor: pointer; 34 | } 35 | 36 | .App__menu-item:not(:last-child) { 37 | border-left: #575f6d; 38 | } 39 | 40 | .App__menu-item:hover { 41 | background-color: #2c3340; 42 | } 43 | 44 | .App__menu-item--selected { 45 | background-color: #242b38; 46 | } 47 | 48 | .App__menu-item--selected:hover { 49 | background-color: #242b38; 50 | } 51 | 52 | .App__content { 53 | display: flex; 54 | } 55 | 56 | .App__microfrontend-content { 57 | flex: 1 0 220px; 58 | } -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/App.js: -------------------------------------------------------------------------------- 1 | import { withMicrofrontend } from '@cmra/react'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | import './App.css'; 6 | 7 | const App = ({ microfrontends }) => { 8 | const [selectedMicrofrontendKey, setSelectedMicrofrontendKey] = useState(undefined); 9 | 10 | const MicrofrontendComponent = selectedMicrofrontendKey 11 | ? microfrontends[selectedMicrofrontendKey] 12 | : Object.values(microfrontends)[0]; 13 | 14 | return ( 15 |
16 |

17 | Welcome to Microfrontend World. 18 |
19 | Choose a microfrontend to show: 20 |

21 |
22 | {Object.keys(microfrontends).map((microfrontend) => ( 23 | 30 | ))} 31 |
32 |
33 | {MicrofrontendComponent && ( 34 |
35 | 36 |
37 | )} 38 |
39 |
40 | ); 41 | }; 42 | export default withMicrofrontend(App); 43 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { ImportMicrofrontend } from '@cmra/react'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP/packages/webapp/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | builds/ 4 | 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CREATE_APP_WITH_MICRO", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "lerna": "<4.0.0", 8 | "@cmra/cli": "^2.1.18" 9 | }, 10 | "scripts": { 11 | "postinstall": "lerna bootstrap", 12 | "build": "yarn build:packages && yarn package", 13 | "build:packages": "cmra build -a webapp", 14 | "package": "cmra build -p webapp", 15 | "start": "cmra start -a webapp" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | build-lib 25 | public/meta.json -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microfrontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@cmra/cli": "^2.1.18", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "@cmra/react": "^2.1.3", 13 | "react-scripts": "3.4.1" 14 | }, 15 | "scripts": { 16 | "start": "cmra start", 17 | "build": "cmra build -m", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject", 20 | "build:standalone": "cmra build -s" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": { 26 | "production": [ 27 | ">0.2%", 28 | "not dead", 29 | "not op_mini all" 30 | ], 31 | "development": [ 32 | "last 1 chrome version", 33 | "last 1 firefox version", 34 | "last 1 safari version" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/logo192.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/logo512.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 |

11 | Edit src/App.js and save to reload. 12 |

13 | 19 | Learn React 20 | 21 |
22 |
23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/index.js: -------------------------------------------------------------------------------- 1 | import { ExportMicrofrontend } from '@cmra/react'; 2 | import App from './App'; 3 | 4 | ExportMicrofrontend({ 5 | name: 'microfrontend', 6 | view: App, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/microfrontend/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | build-lib 25 | public/meta.json -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "@cmra/cli": "^2.1.18", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "@cmra/react": "^2.1.3", 13 | "react-scripts": "3.4.1" 14 | }, 15 | "scripts": { 16 | "start": "cmra start", 17 | "build": "cmra build -s", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/favicon.ico -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/logo192.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/logo512.png -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App__header { 6 | background-color: #282c34; 7 | display: flex; 8 | flex-direction: column; 9 | align-items: center; 10 | justify-content: center; 11 | font-size: calc(10px + 2vmin); 12 | color: white; 13 | font-weight: bold; 14 | padding: 16px; 15 | line-height: 32px; 16 | margin: 0; 17 | } 18 | 19 | .App__menu { 20 | display: flex; 21 | justify-content: center; 22 | background-color: #575f6d; 23 | } 24 | 25 | .App__menu-item { 26 | border: 0; 27 | background-color: #373f4e; 28 | padding: 16px; 29 | color: white; 30 | text-transform: uppercase; 31 | font-weight: bold; 32 | outline: 0; 33 | cursor: pointer; 34 | } 35 | 36 | .App__menu-item:not(:last-child) { 37 | border-left: #575f6d; 38 | } 39 | 40 | .App__menu-item:hover { 41 | background-color: #2c3340; 42 | } 43 | 44 | .App__menu-item--selected { 45 | background-color: #242b38; 46 | } 47 | 48 | .App__menu-item--selected:hover { 49 | background-color: #242b38; 50 | } 51 | 52 | .App__content { 53 | display: flex; 54 | } 55 | 56 | .App__microfrontend-content { 57 | flex: 1 0 220px; 58 | } -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/App.js: -------------------------------------------------------------------------------- 1 | import { withMicrofrontend } from '@cmra/react'; 2 | 3 | import React, { useState } from 'react'; 4 | 5 | import './App.css'; 6 | 7 | const App = ({ microfrontends }) => { 8 | const [selectedMicrofrontendKey, setSelectedMicrofrontendKey] = useState(undefined); 9 | 10 | const MicrofrontendComponent = selectedMicrofrontendKey 11 | ? microfrontends[selectedMicrofrontendKey] 12 | : Object.values(microfrontends)[0]; 13 | 14 | return ( 15 |
16 |

17 | Welcome to Microfrontend World. 18 |
19 | Choose a microfrontend to show: 20 |

21 |
22 | {Object.keys(microfrontends).map((microfrontend) => ( 23 | 30 | ))} 31 |
32 |
33 | {MicrofrontendComponent && ( 34 |
35 | 36 |
37 | )} 38 |
39 |
40 | ); 41 | }; 42 | export default withMicrofrontend(App); 43 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { ImportMicrofrontend } from '@cmra/react'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | -------------------------------------------------------------------------------- /packages/cli/test/__snapshots__/dist/CREATE_APP_WITH_MICRO/packages/webapp/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/cli/test/create-app.test.js: -------------------------------------------------------------------------------- 1 | const dircompare = require('dir-compare'); 2 | const create = require('../scripts/create'); 3 | const { mkdir, rm } = require('../scripts/utils/fs'); 4 | 5 | 6 | const buildFolder = (testName, isOld) => `./test/__snapshots__/dist${isOld ? '' : '-new'}/${testName}`; 7 | 8 | const testSnapshot = async (testName) => { 9 | const newFolder = buildFolder(testName); 10 | const oldFolder = buildFolder(testName, true); 11 | let result; 12 | try { 13 | result = await dircompare.compare(oldFolder, newFolder, { 14 | compareContent: true, 15 | excludeFilter: 'node_modules', 16 | }); 17 | } catch (e) { 18 | throw new Error(`There is no snapshot created for "${testName}". Maybe you should check "${newFolder}" and, if it is right, copy to "${oldFolder}" in order to setup a new base snapshot`); 19 | } 20 | 21 | if (result.distinctFiles > 0) { 22 | throw new Error(` 23 | Test "${testName}" failed with ${result.distinctFiles} different files. Check diff with: 24 | 25 | ${result.diffSet 26 | .filter(diff => diff.state !== 'equal') 27 | .map(diff => `diff ${diff.path1}/${diff.name1} ${diff.path2}/${diff.name2}`) 28 | .join('\n\n')} 29 | `); 30 | } 31 | }; 32 | 33 | describe('test', () => { 34 | beforeAll(async () => { 35 | await rm(buildFolder('')); 36 | await mkdir(buildFolder('', true)); 37 | await mkdir(buildFolder('')); 38 | }); 39 | it('should create app', async () => { 40 | const TEST_NAME = 'CREATE_APP'; 41 | await create([create.TYPE.APP], buildFolder(TEST_NAME), {}); 42 | await testSnapshot(TEST_NAME); 43 | }); 44 | it('should create app', async () => { 45 | const TEST_NAME = 'CREATE_APP_WITH_MICRO'; 46 | await create([create.TYPE.APP, create.TYPE.MICROFRONTEND], buildFolder(TEST_NAME), {}); 47 | await testSnapshot(TEST_NAME); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/doc/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /packages/doc/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /packages/doc/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: backoffice 3 | title: Overview 4 | --- 5 | 6 | import { TodoTag } from './components/tag'; 7 | 8 | **Create Micro Create App** gives you an out of the box solution for managing your microfrontends with a powerful and open source api/webapp that allow you to: 9 | 10 | - Get control of what microfrontend versions are deployed; 11 | - Deploy multiple microfrontends combination, create beta versions and make sure everything is right; 12 | - Integrate with multiple platforms such as github or amazon s3; 13 | - Notify you when things happen on your env. 14 | 15 | ## Why 16 | 17 | Managing microfrontends is not easy and we know that. 18 | 19 | This architecture can make your application chaotic if you don't organize things. The best way of keeping your life with multiple microfrontends and possibly multiple applications, organized is with this simple backoffice. 20 | 21 | ## How to use 22 | 23 | Having [this store example](core/examples.md) as our use case, let's explore how could we use this backoffice. 24 | 25 | - first of all, if you are using github as your artifact storage, you can publish all your packages (webapp and microfrontends) with `publish` command: 26 | ```bash 27 | npx @cmra/cli publish 28 | ``` 29 | - register new application named `My Cool Store` with container name `webapp` (from `webapp/package.json`); 30 | - add new microfrontends related to your application: `cart`, `design-system`, `showcase` and `promotions`; 31 | - all these new microfrontends will have it's own versions synched and ready to be deployed. 32 | - choose which version for each microfrontend you want to deploy 33 | - your application is ready: just go to your github.io page! 34 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/developing.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: developing 3 | title: Developing 4 | --- 5 | 6 | ### Setup your environment 7 | 8 | First of all, you will need a postgresql server to connect to. 9 | 10 | If you are using aws s3 as your artifact's host, login in into aws cli and then startup your backoffice 11 | 12 | Create a .env.development.local with some envs: 13 | 14 | ```bash 15 | # Core configs 16 | 17 | database_config='{"host":"my.host", "port": 5432, "username": "my-user", "password": "my-password", "database": "my-db}' 18 | firebase_admin_config='FIREBASE_ADMIN_CONFIG_JSON' # https://firebase.google.com/docs/admin/setup#initialize_the_sdk 19 | firebase_config='FIREBASE_CONFIG_JSON' # https://firebase.google.com/docs/web/setup#config-object 20 | 21 | # AWS (needed only if you will use aws s3 as your artifact manager or to deploy some application) 22 | aws_profile='my-profile' 23 | AWS_SDK_LOAD_CONFIG=1 # needed to use a profile 24 | AWS_ARTIFACTS_BUCKET='some-bucket-with-all-artifacts' 25 | AWS_REGION='your-amazon-region' 26 | ``` 27 | 28 | Run a script: 29 | 30 | `./scripts/start.js` 31 | 32 | Access your backoffice: http://localhost:3333/ 33 | 34 | ## Generating db migration 35 | 36 | Create a file at `/packages/server/ormconfig.json` with this content: 37 | 38 | ```json 39 | { 40 | "host": "my.host", 41 | "port": 5432, 42 | "username": "my-user", 43 | "password": "my-password", 44 | "database": "my-db", 45 | "type": "postgres", 46 | "migrations": ["migration/*.js"], 47 | "entities": ["./dist/src/entity/*.js"], 48 | "cli": { 49 | "migrationsDir": "migration" 50 | } 51 | } 52 | ``` 53 | 54 | Make sure you have `typeorm` installed globally: 55 | 56 | ```bash 57 | npm i -g typeorm 58 | typeorm migration:generate -n 59 | ``` 60 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/namespace.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: namespace 3 | title: Namespace 4 | --- 5 | 6 | import { WipTag } from './components/tag'; 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/organizing-microfrontends.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: organizing-microfrontends 3 | title: Organizing your microfrontends 4 | --- 5 | 6 | A way you can organize your application: 7 | 8 | Divide your users in groups. Each group must have some examples of all your user types to assure that all namespaces test all your features. 9 | 10 | Create namespaces with each user group, something like: 11 | 12 | - Alpha: 5% 13 | - Beta: 15% 14 | - Stable: 30% 15 | - Production: 50% 16 | 17 | When you finish developing your microfrontend changes, update its latest version. 18 | Schedule your application deploy window to the period where your users rarely uses your system. 19 | Lets suppose these days are: monday, tuesday and wednesday. 20 | 21 | If some of your microfrontends has a new version to be deployed, every chosen day will check any new versions to deploy to next namespace. 22 | To promote a version to another namespace, someone has to check some metrics and then say that all went fine, allowing this version to follow its path. 23 | 24 | Micro A -> v1 25 | Micro B -> v1 26 | Micro C -> v1 27 | 28 | Alpha -> Av1 | Bv1 | Cv1 29 | Beta -> Av1 | Bv1 | Cv1 30 | Stable -> Av1 | Bv1 | Cv1 31 | Production -> Av1 | Bv1 | Cv1 32 | 33 | Monday 34 | 35 | - New A version (v2). 36 | - Alpha updates -> Av2 | Bv1 | Cv1 37 | 38 | Tuesday 39 | 40 | - Ask if everything went well on Alpha 41 | - Someone says it went fine 42 | - Beta updates -> Av2 | Bv1 | Cv1 43 | 44 | Wedneday 45 | 46 | - New C version (v2) 47 | - Alpha updates -> Av2 | Bv1 | Cv2 48 | - Ask if everything went well on Beta 49 | 50 | ... 51 | 52 | Monday 53 | 54 | - Someone says it went fine 55 | - Stable updates -> Av2 | Bv1 | Cv1 56 | - Beta updatee -> Av2 | Bv1 | Cv2 57 | 58 | Tuesday 59 | 60 | - Ask if everything went well on Stable 61 | - Someone says it went fine 62 | - Production updates -> Av2 | Bv1 | Cv1 63 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/release.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: release 3 | title: Release 4 | --- 5 | 6 | asd 7 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/roadmap.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: roadmap 3 | title: Roadmap 4 | --- 5 | 6 | This is not a promise of what is going to be implemented. 7 | This is just a simple overview of cool things that could come up one day. 8 | 9 | Have suggestions of new features? [Open an issue](https://github.com/matheusmr13/create-micro-react-app/issues/new) and/or [contribute](https://github.com/matheusmr13/create-micro-react-app/blob/master/CONTRIBUTING.md)! 10 | 11 | ## Next steps 12 | 13 | - Make it easy to deploy 14 | - server -> create package to extend -> `import Server from '@cmra/server'; Server.run();` 15 | - frontend -> create package to extend -> `import Webapp from '@cmra/web'; await Webapp.build(); Server.addStatic('build');` 16 | - docker image with all together (+heroku) 17 | - Permission 18 | - get things separate between users (currently works if you create your own environment) 19 | - Deploy 20 | - Schedule deploy to date 21 | - Recurrent deploy 22 | - Integrations to publish final package to: 23 | - Amazon s3 24 | - Package management integration, get packages from: 25 | - NPM 26 | - Amazon s3 27 | - Gitlab? 28 | - Extends command line 29 | - Application template (create-micro-react-app --from ) 30 | - CLI for general uses such as preparing a next deploy 31 | - Flux between multiple namespaces 32 | - Define an order for each namespace (such as alpha->beta->main) 33 | - Expand namespaces 34 | - Create hidden "next deploy preview" 35 | - Integrate @cmra/react with setting namespaces 36 | - Custom microfrontends 37 | - Set microfrontends meta infos (nav bar label? url? custom field) 38 | - Lazy loading 39 | - Changelog by version () 40 | - Notifications 41 | - Changes to next deploy on namespace 42 | - Next deploy started (and done) 43 | - Integrations: 44 | - slack 45 | - email 46 | - Cannary deploy Vs Quality checks 47 | - Define some rules to get things checked and move to next deploy (really next to flux iniciative) 48 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/setup-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setup-application 3 | title: Setup application 4 | --- 5 | 6 | Once we have a **Create Micro React App** backoffice up and running, access it and configure your application/microfrontends: 7 | 8 | - Click on "Applications" menu item; 9 | - Click on "New" button; 10 | - Fill up your application's name and container's package name (eg: `webapp`); 11 | - Once in your application's detail page, you should be able to setup your "integration type" and "destination"; 12 | - Integration type: which infrastructure your application is going to be deployed at, eg: amazon s3, github pages, etc; 13 | - Destination: where your application should be deployed at. If you chose amazon s3 as your infrastructure, you should choose which bucket its going to be deployed at. 14 | - Setup your container microfrontend (click on "Edit" inside microfrontend's card): 15 | - Integration type: same as above, but point at where your release artifact is at; 16 | - Origin: where your artifacts are located at (if you are using amazon s3, this list will show folders inside AWS_ARTIFACTS_BUCKET environnement); 17 | - Click on "Save" and then on "Sync Versions" to fetch current released versions for your container. 18 | - You are good to go. Click on deploy and your destination should be updated (: (if you are curious about what happens when you click this button, check ["how it works" doc](https://matheusmr13.github.io/create-micro-react-app/docs/how-it-works)) 19 | 20 | These steps above got you up and running with a simple webapp without any microfrontends connected. Let's setup your first microfrontend? 21 | 22 | - Go to your application's details page; 23 | - Fill up microfrontend info (very similar to above step where you setup webapp's container); 24 | - Go back to your application and deploy it! 25 | -------------------------------------------------------------------------------- /packages/doc/docs/backoffice/setup-environment.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: setup-environment 3 | title: Setup environment 4 | --- 5 | 6 | To setup a backoffice instance inside your infrastructure, just create a npm project and install our server + webapp. 7 | 8 | ```bash 9 | npm init -y 10 | npm i @cmra/server 11 | npm i @cmra/webapp 12 | ``` 13 | 14 | and then define your index.js 15 | 16 | ```js 17 | const NodeApp = require('@cmra/server'); 18 | const Webapp = require('@cmra/webapp'); 19 | 20 | const configJson = { 21 | firebase: { 22 | /* your firebase client configs */ 23 | }, 24 | firebaseAdmin: { 25 | /* your firebase admin configs from google cloud services */ 26 | }, 27 | database: { 28 | /* your database config */ 29 | }, 30 | }; 31 | 32 | const run = async () => { 33 | const destFolder = `${__dirname}/node_modules/@cmra/webapp/build`; 34 | 35 | await Webapp.build({ 36 | env: { 37 | FIREBASE_CONFIG: JSON.stringify(firebaseConfig), 38 | BASE_URL: '', 39 | }, 40 | }); 41 | 42 | NodeApp.withDatabase(configJson.database) 43 | .withFirebaseConfig({ 44 | ...configJson.firebaseAdmin, 45 | private_key: JSON.parse(`"${configJson.firebaseAdmin.private_key}"`), 46 | }) 47 | .withStaticFiles(destFolder) 48 | .run(8080); 49 | }; 50 | 51 | run(); 52 | ``` 53 | -------------------------------------------------------------------------------- /packages/doc/docs/cli.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: cli 3 | title: Overview 4 | --- 5 | 6 | import { BetaTag } from './components/tag'; 7 | 8 | Our cli aims to help you creating, developing and shipping your project. 9 | 10 | ```bash 11 | npx @cmra/cli [command] 12 | ``` 13 | 14 | Core features: 15 | 16 | - `create` enables you creating an entire `app` or just some `microfrontend` or `library`; 17 | - `build` wraps some build logic around `react-scripts`; 18 | - `start` helps with development experience with tools like hot reload and previewing a microfrontend inside a webapp; 19 | - `publish` integrates with supported platforms (eg: github). 20 | 21 | ```bash 22 | npx @cmra/cli --help 23 | npx @cmra/cli start --help 24 | npx @cmra/cli build --help 25 | npx @cmra/cli create --help 26 | ``` 27 | -------------------------------------------------------------------------------- /packages/doc/docs/cli/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: build 3 | title: build 4 | --- 5 | 6 | ```bash 7 | npx @cmra/cli build --help 8 | ``` 9 | -------------------------------------------------------------------------------- /packages/doc/docs/cli/create.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: create 3 | title: create 4 | --- 5 | 6 | ```bash 7 | npx @cmra/cli create --help 8 | ``` 9 | -------------------------------------------------------------------------------- /packages/doc/docs/cli/future.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: future 3 | title: Future 4 | --- 5 | 6 | ## Just brainstorming with ideas 7 | 8 | Cli should become 3 different packages: 9 | 10 | - `scripts` to be used by apps and microfrontends to build/start (`cmra start` and `cmra build`) 11 | - `create` to create modules (`cmra create`) 12 | - `cli` to access backofffice API (does not exist today) 13 | - should `cmra publish` and `cmra deploy` be on cli or scripts? 14 | -------------------------------------------------------------------------------- /packages/doc/docs/cli/publish.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: publish 3 | title: Publish 4 | --- 5 | 6 | This command will help you with publishing your application to some artifact management platform (like github or npm). 7 | 8 | ```bash 9 | npx @cmra/cli publish --help 10 | ``` 11 | 12 | With 0 configs you can publish your build folder to github and allow backoffice to use this info. 13 | 14 | ```bash 15 | npx @cmra/cli publish 16 | ``` 17 | -------------------------------------------------------------------------------- /packages/doc/docs/cli/start.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: start 3 | title: start 4 | --- 5 | 6 | ## All on watch mode 7 | 8 | ### Mono repo 9 | 10 | On your root project just use a script command on your `package.json`, specifing what project is the container. 11 | 12 | ```json 13 | "start": "cmra start -a webapp" 14 | ``` 15 | 16 | ### Multi repo 17 | 18 | Create a json specifing the path of all your packages, `microfrontends.json`: 19 | 20 | ```json 21 | { 22 | "microfrontends": { 23 | "my-microfrontend1": "/path/to/microfrontend1/root", 24 | "my-microfrontend2": "/path/to/microfrontend2/root" 25 | }, 26 | "app": { 27 | "my-webapp": "/path/to/app/root" 28 | } 29 | } 30 | ``` 31 | 32 | On your root project just use a script command on your `package.json`, specifing config file. 33 | 34 | Example: 35 | 36 | ```json 37 | "start": "cmra start -c microfrontends.json" 38 | ``` 39 | 40 | ## Simulate production environment 41 | 42 | (with some microfrontend on watch mode) 43 | 44 | On your specific microfrontend, add a script to your `package.json`: 45 | 46 | ```json 47 | "start:prod": "cmra start -p https://myapp.site.com" 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/doc/docs/components/tag.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Tag = ({ children, color, dark }) => ( 4 | 13 | {children} 14 | 15 | ); 16 | 17 | export const TodoTag = () => ( 18 | 19 | TODO 20 | 21 | ); 22 | export const WipTag = () => WIP; 23 | export const BetaTag = () => BETA; 24 | export default Tag; 25 | -------------------------------------------------------------------------------- /packages/doc/docs/core.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: core 3 | title: Overview 4 | --- 5 | 6 | import { TodoTag } from './components/tag'; 7 | 8 | It works 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/doc/docs/core/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: examples 3 | title: Examples 4 | --- 5 | 6 | ## Store 7 | 8 | Let's use this repository as example: [micro-store](https://github.com/matheusmr13/micro-store); 9 | 10 | [Demo](https://matheusmr13.github.io/micro-store/) 11 | 12 | Here we have a application that represents a simple store: 13 | 14 | - you can browse products 15 | - filter by promotions 16 | - add products to your cart 17 | - access your current cart and remove items 18 | 19 | With this features we divided our application in 5 parts (this is just an example, if you are developing a real page as simple as this example, maybe you should not use microfrotend architechture): 20 | 21 | - `webapp`: our container page that organize all other microfrontends 22 | - `cart`: exports 2 components (a widget and a full page that shows current cart) and an api that enables adding and removing products from cart 23 | - `showcase`: a list of products with an api to setup type filter that calls `cart` api to add products 24 | - `promotion`: list of filters by type that calls `showcase` api 25 | - `design-system`: used by other microfrontends, exports `Button` component 26 | -------------------------------------------------------------------------------- /packages/doc/docs/core/microfrontend.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: microfrontend 3 | title: What are microfrontends? 4 | --- 5 | 6 | ## First of all: what are microfrontends? 7 | 8 | Check it out some cool content about this topic: 9 | 10 | - [Martin Fowler's post about it](https://martinfowler.com/articles/micro-frontends.html) 11 | - [Michael Geers's post with some example code](https://micro-frontends.org/) 12 | - [Single-spa reference](https://single-spa.js.org/docs/microfrontends-concept/) 13 | -------------------------------------------------------------------------------- /packages/doc/docs/development.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: development 3 | title: Development Overview 4 | --- 5 | 6 | import { TodoTag } from './components/tag'; 7 | 8 | It works 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/doc/docs/development/build-and-deploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: build-and-deploy 3 | title: Build and Deploy 4 | --- 5 | 6 | We recommend the following process to build and deploy your application: 7 | 8 | ## First build 9 | 10 | Build all your modules separately, saving dist output somewhere where you can retrieve. 11 | 12 | To build a webapp module (that will be the container to all other microfrontends): 13 | 14 | ```bash 15 | @cmra/cli build -s 16 | ``` 17 | 18 | To build all other microfrontends: 19 | 20 | ```bash 21 | @cmra/cli build 22 | ``` 23 | 24 | ## Then package 25 | 26 | In a separate process, choose which version of each microfrontend you would like to deploy. 27 | Get all files from previous step and put it in a folder named `builds`. It should look something like: 28 | 29 | ``` 30 | - builds 31 | - webapp 32 | - index.html 33 | - ... 34 | - package1 35 | - index.html 36 | - ... 37 | - package2 38 | - index.html 39 | - ... 40 | - ... 41 | ``` 42 | 43 | and then run: 44 | 45 | ```bash 46 | @cmra/cli build -p webapp 47 | ``` 48 | 49 | ## Finally deploy 50 | 51 | Upload `build` folder generated to some static files host (like s3 or even github pages). 52 | -------------------------------------------------------------------------------- /packages/doc/docs/how-it-works.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: how-it-works 3 | title: Overview 4 | --- 5 | 6 | import { TodoTag } from './components/tag'; 7 | 8 | It works 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/doc/docs/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: support 3 | title: <3 4 | --- 5 | 6 | Have a question? Found a problem? 7 | 8 | Don't be shy! [Create an issue](https://github.com/matheusmr13/create-micro-react-app/issues/new) and let's talk about it! <3 9 | -------------------------------------------------------------------------------- /packages/doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "GIT_USER=matheusmr00 docusaurus deploy", 11 | "serve": "docusaurus serve" 12 | }, 13 | "dependencies": { 14 | "@docusaurus/core": "^2.0.0-alpha.61", 15 | "@docusaurus/preset-classic": "^2.0.0-alpha.61", 16 | "@mdx-js/react": "^1.5.8", 17 | "clsx": "^1.1.1", 18 | "react": "^16.8.4", 19 | "react-dom": "^16.8.4" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/doc/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | someSidebar: { 3 | 'Getting started': ['getting-started'], 4 | Core: ['core', 'core/examples'], 5 | 'How it works': ['how-it-works'], 6 | 'Command line interface': ['cli', 'cli/create', 'cli/start', 'cli/build'], 7 | Backoffice: [ 8 | 'backoffice', 9 | 'backoffice/setup-environment', 10 | 'backoffice/setup-application', 11 | 'backoffice/namespace', 12 | 'backoffice/namespace', 13 | ], 14 | Support: ['support'], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/doc/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #845ec2; 11 | --ifm-color-primary-dark: #7449ba; 12 | --ifm-color-primary-darker: #6d43b2; 13 | --ifm-color-primary-darkest: #5a3792; 14 | --ifm-color-primary-light: #9473ca; 15 | --ifm-color-primary-lighter: #9c7dce; 16 | --ifm-color-primary-lightest: #b49dda; 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | 27 | .main-wrapper .button--secondary { 28 | --ifm-font-color-base: var(--ifm-color-primary-darkest); 29 | border-color: var(--ifm-color-primary-darkest); 30 | border-width: 2px; 31 | } 32 | -------------------------------------------------------------------------------- /packages/doc/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | 39 | .heroLogoWhite, 40 | .heroLogo { 41 | width: 150px; 42 | margin-bottom: 40px; 43 | } 44 | 45 | [data-theme='dark'] .heroLogo { 46 | display: none; 47 | } 48 | 49 | .heroLogoWhite { 50 | display: none; 51 | } 52 | 53 | [data-theme='dark'] .heroLogoWhite { 54 | display: initial; 55 | } 56 | 57 | .toolTitle { 58 | margin: auto; 59 | font-size: 30px; 60 | margin-bottom: 40px; 61 | } 62 | 63 | .tool { 64 | padding: 40px 0; 65 | } 66 | 67 | .tool:nth-child(even) { 68 | background-color: var(--ifm-color-primary-darkest); 69 | color: white; 70 | } 71 | 72 | .tool:nth-child(even) pre { 73 | color: var(--ifm-font-color-secondary); 74 | } 75 | 76 | .toolImage { 77 | width: 300px; 78 | margin: 40px auto; 79 | } 80 | 81 | .toolFeatures { 82 | justify-content: center; 83 | } 84 | -------------------------------------------------------------------------------- /packages/doc/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/doc/static/.nojekyll -------------------------------------------------------------------------------- /packages/doc/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/doc/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/e2e/.gitignore: -------------------------------------------------------------------------------- 1 | /e2e-dist 2 | /node_modules 3 | 4 | /cypress/videos 5 | /cypress/screenshots 6 | -------------------------------------------------------------------------------- /packages/e2e/cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/e2e/cypress/integration/checkup.spec.js: -------------------------------------------------------------------------------- 1 | context('Actions', () => { 2 | beforeEach(() => { 3 | cy.visit('localhost:8081'); 4 | }); 5 | 6 | it('should render simple app with menu and one microfrontend', () => { 7 | cy.get('.App__menu-item').should('have.length', 1); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/e2e/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | } 18 | -------------------------------------------------------------------------------- /packages/e2e/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /packages/e2e/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /packages/e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "test": "./src/run.js", 8 | "cypress:open": "cypress open", 9 | "cypress:run": "cypress run" 10 | }, 11 | "dependencies": { 12 | "cypress": "^3.8.2", 13 | "express": "^4.17.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "modules": false 5 | }], 6 | "@babel/preset-react" 7 | ], 8 | "plugins": [ 9 | ["@babel/plugin-proposal-class-properties", { "loose": true }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /packages/react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "env": { 7 | "es6": true, 8 | "browser": true 9 | }, 10 | "plugins": [ 11 | "react" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "max-len": ["error", { 18 | "code": 120 19 | }], 20 | "react/jsx-filename-extension": [1, { 21 | "extensions": [".test.js", ".jsx", "js"] 22 | }], 23 | "arrow-parens": ["as-needed", { 24 | "requireForBlockBody": true 25 | }] 26 | } 27 | } 28 | 29 | // { 30 | // "extends": [ 31 | // "./node_modules/eslint-config-react-app/index.js", 32 | // "airbnb", 33 | // "plugin:cypress/recommended" 34 | // ], 35 | // "parser": "babel-eslint", 36 | // "env": { 37 | // "browser": true, 38 | // "jest": true, 39 | // "cypress/globals": true 40 | // }, 41 | // "plugins": [ 42 | // "react-hooks", 43 | // "cypress" 44 | // ], 45 | // "rules": { 46 | // "comma-dangle": ["error", "never"], 47 | // "react/no-array-index-key": 0, 48 | // "react/jsx-filename-extension": [1, { 49 | // "extensions": [".test.js", ".jsx", "js"] 50 | // }], 51 | // "jsx-a11y/no-autofocus": 0, 52 | 53 | // "jsx-a11y/click-events-have-key-events": 0, 54 | // "lines-between-class-members": 0, 55 | // "react-hooks/rules-of-hooks": "error", 56 | // "react-hooks/exhaustive-deps": "warn", 57 | // "jsx-a11y/no-noninteractive-tabindex": "off" 58 | // }, 59 | // "settings": { 60 | // "import/resolver": { 61 | // "node": { 62 | // "paths": ["src"] 63 | // } 64 | // } 65 | // }, 66 | // } 67 | -------------------------------------------------------------------------------- /packages/react/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | coverage 12 | 13 | # misc 14 | .DS_Store 15 | .env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @cmra/react 2 | 3 | A component to export and import microfrontends on your react application 4 | 5 | ## Import 6 | 7 | Just use this way: 8 | 9 | ``` 10 | // index.js 11 | 12 | import { ImportMicrofrontend } from '@cmra/react'; 13 | 14 | ReactDOM.render(( 15 | 16 | 17 | 18 | ), document.getElementById('root')); 19 | 20 | 21 | // App.js 22 | import { withMicrofrontend } from '@cmra/react'; 23 | 24 | const App = ({ microfrontends }) => ( 25 |
26 | { 27 | Object.keys(microfrontends).map(microfrontend => ( 28 |
29 | {microfrontend.content} 30 |
31 | )) 32 | } 33 |
34 | ); 35 | 36 | export default withMicrofrontend(App); 37 | ``` 38 | 39 | ## Export 40 | 41 | On your microfrontend `index.js` file: 42 | 43 | ``` 44 | import App from './App'; 45 | import { ExportMicrofrontend } from '@cmra/react'; 46 | 47 | ExportMicrofrontend(App); 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /packages/react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cmra/react", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "module": "dist/index.es.js", 9 | "jsnext:main": "dist/index.es.js", 10 | "typings": "dist/index.d.ts", 11 | "files": [ 12 | "dist" 13 | ], 14 | "engines": { 15 | "node": ">=8", 16 | "npm": ">=5" 17 | }, 18 | "scripts": { 19 | "test": "cross-env CI=true react-scripts test --env=jsdom", 20 | "test:watch": "react-scripts test --env=jsdom", 21 | "build": "rollup -c", 22 | "start": "rollup -c -w", 23 | "predeploy": "cd example && yarn install && yarn run build", 24 | "deploy": "gh-pages -d example/build", 25 | "prepare": "yarn run build" 26 | }, 27 | "peerDependencies": { 28 | "react": ">=16.12.0", 29 | "react-dom": ">=16.12.0" 30 | }, 31 | "devDependencies": { 32 | "@svgr/rollup": "^5.0.1", 33 | "@types/enzyme": "^3.10.5", 34 | "@types/jest": "^25.1.3", 35 | "@types/react": "^16.9.23", 36 | "cross-env": "^6.0.3", 37 | "enzyme": "^3.11.0", 38 | "enzyme-adapter-react-16": "^1.15.2", 39 | "gh-pages": "^2.1.1", 40 | "react": "^16.13.0", 41 | "react-dom": "^16.13.0", 42 | "react-scripts": "^3.3.0", 43 | "rollup": "^1.27.14", 44 | "rollup-plugin-cleaner": "^1.0.0", 45 | "rollup-plugin-cleanup": "^3.1.1", 46 | "rollup-plugin-commonjs": "^10.1.0", 47 | "rollup-plugin-node-resolve": "^5.2.0", 48 | "rollup-plugin-postcss": "^2.0.3", 49 | "rollup-plugin-typescript2": "^0.25.3", 50 | "rollup-plugin-url": "^3.0.1", 51 | "typescript": "^3.8.3" 52 | }, 53 | "dependencies": { 54 | "react-helmet": "^5.2.1", 55 | "react-redux": "^7.1.3", 56 | "react-router-dom": "^5.1.2", 57 | "redux": "^4.0.5", 58 | "redux-actions": "^2.6.5", 59 | "redux-thunk": "^2.3.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/react/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs' 2 | import postcss from 'rollup-plugin-postcss' 3 | import resolve from 'rollup-plugin-node-resolve' 4 | import url from 'rollup-plugin-url' 5 | import svgr from '@svgr/rollup' 6 | import cleaner from 'rollup-plugin-cleaner'; 7 | import cleanup from 'rollup-plugin-cleanup'; 8 | import typescript from 'rollup-plugin-typescript2'; 9 | 10 | import pkg from './package.json' 11 | 12 | const external = [ 13 | 'react', 14 | 'react-dom' 15 | ]; 16 | 17 | export default { 18 | input: 'src/index.tsx', 19 | output: [ 20 | { 21 | file: pkg.main, 22 | format: 'cjs', 23 | sourcemap: true 24 | }, 25 | { 26 | file: pkg.module, 27 | format: 'es', 28 | sourcemap: true 29 | } 30 | ], 31 | external, 32 | plugins: [ 33 | cleanup(), 34 | postcss({ 35 | modules: true 36 | }), 37 | url(), 38 | svgr(), 39 | resolve(), 40 | typescript({ 41 | objectHashIgnoreUnknownHack: true, 42 | rollupCommonJSResolveHack: true, 43 | clean: true 44 | }), 45 | commonjs({ 46 | include: 'node_modules/**', 47 | namedExports: { 48 | 'node_modules/react-is/index.js': ['isValidElementType', 'isContextConsumer'] 49 | } 50 | }) 51 | ].concat(cleaner({ targets: ['dist/'] })) 52 | } 53 | -------------------------------------------------------------------------------- /packages/react/src/api/schema/function.ts: -------------------------------------------------------------------------------- 1 | import Meta from "./meta"; 2 | 3 | class FunctionMeta extends Meta { 4 | callMethod = (...args) => { 5 | const callback = this.getShared('callback'); 6 | callback.apply(null, args); 7 | } 8 | 9 | calledMethod = (callback) => { 10 | this.setShared('callback', callback); 11 | } 12 | 13 | build() { 14 | const name = this.getCapitalizedName(); 15 | return { 16 | [`call${name}`]: this.callMethod, 17 | [`on${name}Called`]: this.calledMethod 18 | }; 19 | } 20 | }; 21 | 22 | export default FunctionMeta; 23 | -------------------------------------------------------------------------------- /packages/react/src/api/schema/meta.ts: -------------------------------------------------------------------------------- 1 | import connector, { dispatcher } from '../state/connector'; 2 | 3 | enum ACCESS { 4 | PUBLIC, 5 | PRIVATE, 6 | } 7 | class Meta { 8 | static ACCESS = ACCESS; 9 | name: string; 10 | packageName: string; 11 | access: ACCESS; 12 | private shared: any; 13 | 14 | constructor(props, shared) { 15 | this.name = props.name; 16 | this.packageName = props.packageName; 17 | this.access = props.access || Meta.ACCESS.PRIVATE; 18 | this.shared = shared; 19 | } 20 | 21 | getCapitalizedName() { 22 | return `${this.name.charAt(0).toUpperCase()}${this.name.substring(1)}`; 23 | } 24 | 25 | build() { 26 | throw new Error('Not implemented'); 27 | } 28 | 29 | getShared(key) { 30 | return this.shared.get(key); 31 | } 32 | setShared(key, value) { 33 | this.shared.set(key, value); 34 | } 35 | updateShared(key, map) { 36 | const oldValue = this.getShared(key); 37 | const newValue = map(oldValue); 38 | this.setShared(key, newValue); 39 | return newValue; 40 | } 41 | 42 | connectMethod = (component) => { 43 | return connector(component, this.packageName, this.name); 44 | }; 45 | 46 | dispatch = (action, payload) => { 47 | dispatcher(action, payload); 48 | }; 49 | 50 | static create(Clazz, props, shared) { 51 | return new Clazz(props, shared); 52 | } 53 | } 54 | 55 | export default Meta; 56 | -------------------------------------------------------------------------------- /packages/react/src/api/schema/topic.ts: -------------------------------------------------------------------------------- 1 | import Meta from "./meta"; 2 | 3 | class TopicMeta extends Meta { 4 | publishToSubscribers = (...args) => { 5 | const subscribers = this.getShared('subscribers') || []; 6 | subscribers.forEach((subscriber) => { 7 | subscriber.apply(null, args); 8 | }); 9 | } 10 | 11 | addSubscriber = (callback : any) => { 12 | this.updateShared('subscribers', (subscribers : Array = []) => subscribers.concat([callback])); 13 | } 14 | 15 | build() { 16 | const name = this.getCapitalizedName(); 17 | return { 18 | [`publishOn${name}`]: this.publishToSubscribers, 19 | [`subscribeTo${name}`]: this.addSubscriber 20 | }; 21 | } 22 | }; 23 | 24 | export default TopicMeta; 25 | -------------------------------------------------------------------------------- /packages/react/src/api/shared.ts: -------------------------------------------------------------------------------- 1 | class Shared { 2 | shared = {}; 3 | sharedName = ''; 4 | 5 | constructor(sharedName) { 6 | this.sharedName = sharedName; 7 | const allShared = window['__shared__'] || {}; 8 | this.shared = allShared[sharedName] || {}; 9 | 10 | 11 | allShared[sharedName] = this.shared; 12 | window['__shared__'] = allShared; 13 | } 14 | 15 | set(key, value) { 16 | this.shared[key] = value; 17 | } 18 | 19 | get(key) { 20 | return this.shared[key]; 21 | } 22 | 23 | withScope(scope) { 24 | return new Shared(`${this.sharedName}.${scope}`); 25 | } 26 | 27 | } 28 | 29 | export default Shared; 30 | -------------------------------------------------------------------------------- /packages/react/src/api/state/connector.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Shared from '../shared'; 3 | 4 | const sharedState = new Shared('__state__'); 5 | 6 | 7 | const connector = (Component, packageName, name) => { 8 | const mapStateToProps = (state) => ({ 9 | [name]: state[packageName][name] 10 | }); 11 | const mapDispatchToProps = {}; 12 | 13 | return class MyComponent extends React.Component<{}, { 14 | Component ?: React.ComponentType 15 | }> { 16 | state = { 17 | Component: undefined 18 | } 19 | 20 | componentDidMount() { 21 | const connect = sharedState.get('connector'); 22 | this.setState({ 23 | Component: connect( 24 | mapStateToProps, 25 | mapDispatchToProps 26 | )(Component) 27 | }) 28 | } 29 | 30 | render() { 31 | const { Component } = this.state; 32 | if (!Component) return null; 33 | 34 | // Changing this to "return would cause an error" (???) 35 | const TypescriptFix = ({ C, ...props }) => ; 36 | return 37 | } 38 | } 39 | } 40 | 41 | export const dispatcher = (action, payload) => { 42 | const store = sharedState.get('store'); 43 | if (!store) return; // TODO: check cases where store will not exists 44 | 45 | store.dispatch({ type: action, payload }); 46 | } 47 | export default connector; 48 | -------------------------------------------------------------------------------- /packages/react/src/api/state/provider.tsx: -------------------------------------------------------------------------------- 1 | import { Provider } from 'react-redux'; 2 | import React, { Component } from 'react'; 3 | import createStore from './redux'; 4 | import Api from '..'; 5 | 6 | class ApiProvider extends Component<{ 7 | microfrontends: { [key: string]: Api; } 8 | }> { 9 | state = { 10 | store: null 11 | } 12 | 13 | componentDidMount() { 14 | const { microfrontends } = this.props; 15 | const store = createStore(); 16 | 17 | Object.values(microfrontends).forEach(microfrontend => { 18 | store.injectReducer(microfrontend.getName(), microfrontend.getReducers(), microfrontend.getInitialState()); 19 | }); 20 | 21 | this.setState({ store }); 22 | } 23 | 24 | render() { 25 | const { store } = this.state; 26 | if (!store) return null; 27 | 28 | return ( 29 | 30 | {this.props.children} 31 | 32 | ) 33 | } 34 | } 35 | 36 | export default ApiProvider; 37 | -------------------------------------------------------------------------------- /packages/react/src/api/state/redux.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { combineReducers, createStore as createReduxStore, applyMiddleware } from 'redux'; 3 | import { handleActions } from 'redux-actions'; 4 | import Shared from '../shared'; 5 | import { connect, Provider } from 'react-redux'; 6 | 7 | const sharedState = new Shared('__state__'); 8 | 9 | const staticReducers = { 10 | root: (state = {}, action) => state, 11 | }; 12 | 13 | function configureStore() { 14 | const store: any = createReduxStore(createReducer({})) 15 | 16 | store.asyncReducers = {} 17 | 18 | store.injectReducer = (key, asyncReducer, initialState) => { 19 | store.asyncReducers[key] = handleActions(asyncReducer, initialState); 20 | store.replaceReducer(createReducer(store.asyncReducers)) 21 | } 22 | 23 | sharedState.set('connector', connect); 24 | sharedState.set('store', store); 25 | return store 26 | } 27 | 28 | function createReducer(asyncReducers) { 29 | return combineReducers({ 30 | ...staticReducers, 31 | ...asyncReducers 32 | }) 33 | } 34 | 35 | export default configureStore; 36 | -------------------------------------------------------------------------------- /packages/react/src/api/test/state.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import Api from '..'; 4 | import ApiProvider from '../state/provider'; 5 | 6 | const test = new Api({ 7 | interface: { 8 | myProp: {} 9 | } 10 | }, { 11 | packageName: 'test' 12 | }); 13 | 14 | 15 | describe('State', () => { 16 | describe('property', () => { 17 | const mylib = test.build(Api.ACCESS.PUBLIC_API); 18 | 19 | const MyComponent = (props) => ( 20 | {props.myProp} 21 | ); 22 | 23 | it('should create lib with simple methods', () => { 24 | expect(mylib).toHaveProperty('withMyProp'); 25 | }); 26 | it('should connect component with prop', () => { 27 | const Wrapped = mylib.withMyProp(MyComponent); 28 | const wrapper = mount(( 29 | 30 | 31 | 32 | )); 33 | 34 | mylib.setMyProp('my cool text'); 35 | wrapper.update(); 36 | expect(wrapper.text()).toEqual('my cool text') 37 | 38 | mylib.setMyProp('my another text'); 39 | wrapper.update(); 40 | expect(wrapper.text()).toEqual('my another text') 41 | }); 42 | it('should connect and listen to changes on props', () => { 43 | const Wrapped = mylib.withMyProp(({ myProp }) => ( 44 |
45 | 46 | {myProp} 47 |
48 | )); 49 | const wrapper = mount(( 50 | 51 | 52 | 53 | )); 54 | 55 | mylib.setMyProp(0); 56 | wrapper.update(); 57 | 58 | expect(wrapper.find('span').text()).toEqual('0'); 59 | expect(wrapper.find('button').simulate('click')); 60 | expect(wrapper.find('span').text()).toEqual('1'); 61 | expect(wrapper.find('button').simulate('click')); 62 | expect(wrapper.find('span').text()).toEqual('2'); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/react/src/base/communication.ts: -------------------------------------------------------------------------------- 1 | class Communication { 2 | static TYPE = { 3 | SCRIPT: 'SCRIPT', 4 | STYLE: 'STYLE', 5 | LOAD: 'LOAD', 6 | }; 7 | 8 | static COMMUNICATION_SOURCE = '@cmra/react'; 9 | } 10 | 11 | export default Communication; 12 | -------------------------------------------------------------------------------- /packages/react/src/base/fetch-retry.ts: -------------------------------------------------------------------------------- 1 | const LIMIT = 1; 2 | 3 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 4 | const fetchRetry = async (url, { limit = LIMIT, delay = 5000, ...opts } = {}) => { 5 | let error; 6 | try { 7 | const response = await fetch(url, opts); 8 | const { status, statusText } = response; 9 | 10 | if (status >= 200 && status < 300) { 11 | return response.json(); 12 | } 13 | 14 | error = new Error(statusText); 15 | } catch (_err) { 16 | error = _err; 17 | } 18 | 19 | const shouldRetry = limit > 0; 20 | if (!shouldRetry) throw error; 21 | 22 | if (delay) await sleep(delay); 23 | 24 | return await fetchRetry(url, { limit: limit - 1, delay, ...opts }); 25 | } 26 | 27 | export default fetchRetry 28 | -------------------------------------------------------------------------------- /packages/react/src/container/communication.ts: -------------------------------------------------------------------------------- 1 | import Communication from '../base/communication'; 2 | 3 | class AppClient extends Communication { 4 | from: string; 5 | to: string; 6 | callback: any; 7 | 8 | constructor() { 9 | super(); 10 | this.from = process.env.REACT_APP_PACKAGE_NAME || ''; //TODO: fix this var 11 | this.to = 'http://localhost:3000'; 12 | } 13 | 14 | initialize() { 15 | window.addEventListener( 16 | 'message', 17 | (event) => { 18 | if (!event.data || !event.data.source || event.data.source !== Communication.COMMUNICATION_SOURCE) return; 19 | 20 | this.notifyMessage(event.data); 21 | }, 22 | false 23 | ); 24 | return this; 25 | } 26 | 27 | onMessage(callback) { 28 | this.callback = callback; 29 | return this; 30 | } 31 | 32 | notifyMessage(message) { 33 | this.callback(message); 34 | } 35 | } 36 | 37 | export default AppClient; 38 | -------------------------------------------------------------------------------- /packages/react/src/container/context/consumer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MicrofrontendContext from './index'; 3 | import MICRO_TYPE from '../../renderer/type'; 4 | import Api from '../../api'; 5 | 6 | 7 | const { Consumer: MicrofrontendContextConsumer } = MicrofrontendContext; 8 | 9 | interface withMicrofrontendOptions { 10 | microfrontendKey?: string, 11 | filterByType?: MICRO_TYPE 12 | } 13 | 14 | export default (WrappedComponent, opts: withMicrofrontendOptions = {}) => (props) => ( 15 | 16 | {(microfrontends: { [key: string]: Api; }) => { 17 | 18 | let microfrontendsToPass: Array | null = null; 19 | const { filterByType } = opts; 20 | if (filterByType) { 21 | microfrontendsToPass = Object.values(microfrontends).filter(m => m.hasType(filterByType)); 22 | } 23 | 24 | if (!microfrontendsToPass) { 25 | microfrontendsToPass = Object.values(microfrontends); 26 | } 27 | 28 | return ( } 33 | {...(opts.microfrontendKey ? { microfrontend: microfrontends[opts.microfrontendKey] } : {})} 34 | /> 35 | ); 36 | }} 37 | 38 | ); 39 | -------------------------------------------------------------------------------- /packages/react/src/container/context/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const MicrofrontendContext = React.createContext({}); 3 | export default MicrofrontendContext; 4 | -------------------------------------------------------------------------------- /packages/react/src/container/context/provider.ts: -------------------------------------------------------------------------------- 1 | import MicrofrontendContext from './index'; 2 | 3 | const { Provider: MicrofrontendContextProvider } = MicrofrontendContext; 4 | 5 | export default MicrofrontendContextProvider; 6 | -------------------------------------------------------------------------------- /packages/react/src/container/controller/microfrontend.ts: -------------------------------------------------------------------------------- 1 | import Api from '../../api'; 2 | enum STATUS { 3 | CREATED, // Has microfrontend on meta.json 4 | DISCOVERED, // ? 5 | LOADED, // ? Registered itself on container 6 | IMPORTED, // Has received files from microfrontend instance 7 | REGISTERED, // 8 | INITIALIZED, 9 | }; 10 | 11 | interface Files { 12 | js ?: Array; 13 | css ?: Array 14 | } 15 | 16 | class Microfrontend { 17 | static STATUS = STATUS; 18 | 19 | status : STATUS; 20 | 21 | name : string; 22 | host : string; 23 | isLoaded : boolean = false; 24 | 25 | files : Files = {}; 26 | style = [] 27 | 28 | api ?: Api; 29 | 30 | errorInitializing ?: { type: any; error: any; }; 31 | 32 | constructor(name, metaInfo) { 33 | this.name = name; 34 | this.host = metaInfo.host; 35 | this.files.js = metaInfo.js; 36 | this.files.css = metaInfo.css; 37 | 38 | if ((this.files.js?.length || 0) > 0) { 39 | this.status = STATUS.IMPORTED; 40 | } else { 41 | this.status = STATUS.CREATED; 42 | } 43 | } 44 | 45 | registerApi(schema) { 46 | this.status = STATUS.REGISTERED; 47 | this.api = new Api(schema, {}); 48 | } 49 | 50 | setAsInitialized() { 51 | this.status = STATUS.INITIALIZED; 52 | } 53 | 54 | trackError(type, error) { 55 | this.errorInitializing = { 56 | type, 57 | error 58 | }; 59 | } 60 | 61 | loaded() { 62 | this.status = STATUS.LOADED; 63 | this.isLoaded = true; 64 | } 65 | 66 | importScript(jsScripts) { 67 | this.files.js = jsScripts; 68 | this.status = STATUS.IMPORTED; 69 | } 70 | 71 | hasBeenLoaded() { 72 | return this.isLoaded; 73 | } 74 | 75 | isReady() { 76 | return this.status === STATUS.REGISTERED; 77 | } 78 | 79 | setStyle(style) { 80 | this.style = style; 81 | } 82 | } 83 | 84 | export default Microfrontend; 85 | -------------------------------------------------------------------------------- /packages/react/src/index.tsx: -------------------------------------------------------------------------------- 1 | export { default as Api } from './api'; 2 | export { default as ExportMicrofrontend } from './microfrontend'; 3 | export { default as ImportMicrofrontend } from './container'; 4 | export { default as withMicrofrontend } from './container/context/consumer'; 5 | export { default as TYPE } from './renderer/type'; 6 | -------------------------------------------------------------------------------- /packages/react/src/microfrontend/communication.tsx: -------------------------------------------------------------------------------- 1 | import Communication from '../base/communication'; 2 | 3 | class MicrofrontendClient extends Communication { 4 | from: string; 5 | to: string; 6 | 7 | constructor() { 8 | super(); 9 | this.from = process.env.REACT_APP_PACKAGE_NAME || ''; // TODO: fix to use correct from 10 | this.to = 'http://localhost:3000'; 11 | } 12 | 13 | send(type, payload?: any) { 14 | window.parent.postMessage({ 15 | type, 16 | origin: this.from, 17 | payload, 18 | source: Communication.COMMUNICATION_SOURCE 19 | }, this.to); 20 | } 21 | } 22 | 23 | export default MicrofrontendClient; 24 | -------------------------------------------------------------------------------- /packages/react/src/microfrontend/index.tsx: -------------------------------------------------------------------------------- 1 | import Communication from './communication'; 2 | 3 | const getScriptSrcs = () => { 4 | let jsSrcs: Array = []; 5 | document.querySelectorAll('script').forEach(scriptTag => { 6 | jsSrcs.push(scriptTag.src.toString()); 7 | }); 8 | return jsSrcs; 9 | } 10 | 11 | const ExportMicrofrontend = (objectToExport) => { 12 | const registerMicrofrontend = window['__shared__']['__core__']['registerMicrofrontend']; 13 | 14 | if (registerMicrofrontend) { 15 | registerMicrofrontend(objectToExport.name, objectToExport); 16 | } else { 17 | const communicate = new Communication(); 18 | communicate.send(Communication.TYPE.LOAD); 19 | communicate.send(Communication.TYPE.SCRIPT, getScriptSrcs()); 20 | 21 | const mutationObserver = new MutationObserver(function (mutations) { 22 | setTimeout(() => { 23 | const styleList: Array = []; 24 | document.querySelectorAll('style').forEach(a => styleList.push(a.innerHTML)); 25 | 26 | communicate.send(Communication.TYPE.STYLE, styleList); 27 | }, 100) 28 | }); 29 | const head = document.querySelector('head'); 30 | 31 | if (head) { 32 | mutationObserver.observe(head.getRootNode(), { 33 | childList: true, 34 | subtree: true 35 | }); 36 | } 37 | } 38 | } 39 | 40 | export default ExportMicrofrontend; 41 | -------------------------------------------------------------------------------- /packages/react/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/react/src/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RoutingRenderer from './route'; 3 | import Api from '../api'; 4 | 5 | import MICRO_TYPE from './type'; 6 | 7 | class MicrofrontendRenderer extends React.Component<{ 8 | microfrontends: { [key: string]: Api; }, 9 | type?: MICRO_TYPE 10 | children?: string 11 | }> { 12 | render() { 13 | const { type = MICRO_TYPE.DEFAULT, children, microfrontends } = this.props; 14 | const options = { 15 | [MICRO_TYPE.ROUTING]: () => m.hasType(MICRO_TYPE.ROUTING))} />, 16 | [MICRO_TYPE.DEFAULT]: () => undefined, 17 | }; 18 | let a = options[type](); 19 | return a; 20 | } 21 | } 22 | 23 | export default MicrofrontendRenderer; 24 | -------------------------------------------------------------------------------- /packages/react/src/renderer/route.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Link 7 | } from "react-router-dom"; 8 | import Api from "../api"; 9 | 10 | class App extends React.Component<{ 11 | microfrontends: Array 12 | }> { 13 | render() { 14 | const { microfrontends } = this.props; 15 | return ( 16 | 17 | 18 | {microfrontends.map(micro => ( 19 | 20 | ))} 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /packages/react/src/renderer/type.ts: -------------------------------------------------------------------------------- 1 | enum MICRO_TYPE { 2 | ROUTING, 3 | DEFAULT 4 | } 5 | 6 | export default MICRO_TYPE; 7 | -------------------------------------------------------------------------------- /packages/react/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | Enzyme.configure({ adapter: new Adapter() }); 5 | 6 | const mockHelmet = (helmet) => { 7 | window.helmet = helmet; 8 | } 9 | jest.mock('react-helmet', () => ({ 10 | Helmet: ({ children } = {}) => { 11 | if (children) { 12 | const js = (children[0] || []).map(script => script.props); 13 | const css = (children[1] || ([])).map(style => style.props); 14 | let inlineCss = []; 15 | if (children[2]) { 16 | inlineCss = children[2].reduce((agg, styles) => agg.concat(styles.map(style => style.props)), []); 17 | } 18 | 19 | mockHelmet({ js, css, inlineCss }); 20 | } 21 | 22 | return null; 23 | } 24 | })); 25 | -------------------------------------------------------------------------------- /packages/react/src/test/mock/microfrontend/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import schema from './schema'; 3 | import api from './lib'; 4 | export default { 5 | ...schema, 6 | view: { 7 | myView: () =>
8 | micro-content 9 | {api.getMyProp()} 10 | 11 |
12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /packages/react/src/test/mock/microfrontend/lib.ts: -------------------------------------------------------------------------------- 1 | import schema from './schema'; 2 | import { Api } from '../../../index.tsx'; 3 | export default new Api(schema).build(Api.ACCESS.PRIVATE_API); 4 | -------------------------------------------------------------------------------- /packages/react/src/test/mock/microfrontend/schema.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export default { 3 | name: 'my-micro', 4 | interface: { 5 | myProp: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/react/src/test/mock/webapp/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withMicrofrontend } from '../../../index'; 3 | import MicrofrontendLib from '../microfrontend/lib'; 4 | 5 | const App = withMicrofrontend(MicrofrontendLib.withMyProp(({ microfrontend, myProp }) => ( 6 |
7 | content 8 | {myProp} 9 | {} 10 |
11 | )), { microfrontendKey: 'my-micro' }); 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "target": "es5", 6 | "lib": [ 7 | "es6", 8 | "dom", 9 | "es2016", 10 | "es2017" 11 | ], 12 | "sourceMap": true, 13 | "allowJs": false, 14 | "jsx": "react", 15 | "declaration": true, 16 | "moduleResolution": "node", 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true, 19 | "skipLibCheck": true, 20 | "strict": true, 21 | "forceConsistentCasingInFileNames": true, 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "noEmit": true, 25 | "noImplicitAny": false 26 | }, 27 | "baseUrl": "./src", 28 | "paths": { 29 | "src/*": [ 30 | "src/*" 31 | ] 32 | }, 33 | "exclude": [ 34 | "node_modules", 35 | "dist" 36 | ], 37 | "include": [ 38 | "src" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/server/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /packages/server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /dist 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | ormconfig.json 25 | 26 | firebase-admin-sdk-key.json -------------------------------------------------------------------------------- /packages/server/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs12 2 | env: standard 3 | 4 | handlers: 5 | - url: / 6 | static_files: ../marketplace-frontend/build/\1 7 | upload: ../marketplace-frontend/build/(.*) 8 | -------------------------------------------------------------------------------- /packages/server/cases.md: -------------------------------------------------------------------------------- 1 | # microfrontend-marketplace 2 | 3 | ## Actors 4 | 5 | [WAD] => WebApp Dev 6 | 7 | [MFD] => MicroFrontend Dev 8 | 9 | [SYS] => System 10 | 11 | # Fluxogram 12 | 13 | ## Setup 14 | 15 | [WAD] Create repo X 16 | 17 | [WAD] `> create-micro-react-app my-app` 18 | 19 | [WAD] Login with github on **micro-marketplace** 20 | 21 | [SYS] Get repos from user 22 | 23 | [WAD] Choose repo X as **application** 24 | 25 | [SYS] Create `application` 26 | 27 | [WAD] `> micro-marketplace publish` 28 | 29 | [SYS] Setup container version 30 | 31 | ## Plugin creation 32 | 33 | [MFD] Create repo Y 34 | 35 | [MFD] `> create-micro-react-app my-micro` 36 | 37 | [MFD] Login with github on **micro-marketplace** 38 | 39 | [SYS] Get repos from user 40 | 41 | [MFD] Choose repo Y as **microfrontend** and a **application** 42 | 43 | [MFD] `> micro-marketplace publish` 44 | 45 | [SYS] Upload version artifact to `versions` branch in a folder like `/versions/1.0.0` 46 | 47 | [SYS] Create new **version** for this **microfrontend** 48 | 49 | ## Deploy 50 | 51 | [SYS] Get all versions from all microfrontend repositories 52 | 53 | [SYS] Package with `@cmra/cli build -p` 54 | -------------------------------------------------------------------------------- /packages/server/decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'octokat'; 2 | declare namespace Express { 3 | export interface Request { 4 | locals?: { 5 | user: import('./src/account/user').default; 6 | }; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | db: 5 | image: 'postgres:12' 6 | restart: 'always' 7 | ports: 8 | - '5432:5432' 9 | environment: 10 | POSTGRES_PASSWORD: 'password' 11 | POSTGRES_USER: 'typeormtest' 12 | -------------------------------------------------------------------------------- /packages/server/lib/index.js: -------------------------------------------------------------------------------- 1 | const server = require('../dist/src/server'); 2 | 3 | module.exports = server.default; 4 | -------------------------------------------------------------------------------- /packages/server/migration/1598386312520-ProjectLink.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class ProjectLink1598386312520 implements MigrationInterface { 4 | name = 'ProjectLink1598386312520'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query(`ALTER TABLE "microfrontend" ADD "projectLink" character varying`); 8 | } 9 | 10 | public async down(queryRunner: QueryRunner): Promise { 11 | await queryRunner.query(`ALTER TABLE "microfrontend" DROP COLUMN "projectLink"`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cmra/server", 3 | "version": "1.0.7", 4 | "description": "", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "prepare": "npm run build", 8 | "pretest": "npm run build", 9 | "posttest": "npm run lint", 10 | "test": "echo 'pass'", 11 | "lint": "tslint -p .", 12 | "start": "NODE_PATH=./dist node ./dist/src/index.js", 13 | "start:watch": "npm run build:watch & NODE_PATH=./dist nodemon ./dist/src/index.js", 14 | "build": "tsc -p . --outDir ./dist", 15 | "build:watch": "npm run build -- --watch", 16 | "typeorm": "ts-node --files ./src ./node_modules/typeorm/cli.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/matheusmr13/microfrontend-marketplace.git" 21 | }, 22 | "author": "", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/matheusmr13/microfrontend-marketplace/issues" 26 | }, 27 | "homepage": "https://github.com/matheusmr13/microfrontend-marketplace#readme", 28 | "dependencies": { 29 | "@octokit/rest": "^17.6.0", 30 | "@slack/web-api": "^5.9.0", 31 | "@types/cors": "^2.8.6", 32 | "@types/express": "^4.17.4", 33 | "@types/mime-types": "^2.1.0", 34 | "@types/node": "^14.0.6", 35 | "@types/tar-fs": "^2.0.0", 36 | "@types/testing-library__dom": "^7.5.0", 37 | "@types/uuid": "^7.0.2", 38 | "aws-sdk": "^2.719.0", 39 | "axios": "^0.19.2", 40 | "cors": "^2.8.5", 41 | "dayjs": "^1.8.24", 42 | "express": "^4.17.1", 43 | "firebase-admin": "^8.12.1", 44 | "mime-types": "^2.1.27", 45 | "octokat": "^0.10.0", 46 | "pg": "^8.2.1", 47 | "reflect-metadata": "^0.1.13", 48 | "tar-fs": "^2.1.0", 49 | "typeorm": "^0.2.25", 50 | "typescript": "^3.9.7", 51 | "uuid": "^7.0.3", 52 | "zlib": "^1.0.5" 53 | }, 54 | "devDependencies": { 55 | "nodemon": "^2.0.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/server/src/account/controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../base/controller'; 2 | import ForbiddenError from '../base/errors/forbidden'; 3 | import User from '../entity/user'; 4 | 5 | class UserController extends BaseController { 6 | constructor() { 7 | super(User); 8 | } 9 | 10 | public getExtra = this.withContext(async (req, res, context) => { 11 | const user = await context.getUser(); 12 | if (user.id !== req.params.uuid) throw new ForbiddenError(); 13 | const extra = await user.getExtra(); 14 | res.json(extra); 15 | }); 16 | 17 | public updateExtra = this.withContext(async (req, res, context) => { 18 | const user = await context.getUser(); 19 | if (user.id !== req.params.uuid) throw new ForbiddenError(); 20 | let extra = await user.getExtra(); 21 | 22 | extra = User.merge(extra, req.body); 23 | 24 | extra = await extra.save(); 25 | res.json(extra); 26 | }); 27 | } 28 | 29 | export default new UserController(); 30 | -------------------------------------------------------------------------------- /packages/server/src/account/filter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import FirebaseWrapper from './firebase-wrapper'; 3 | import User from './user'; 4 | 5 | const AuthFilter = async (req: Request, res: Response, next: NextFunction) => { 6 | console.log(req.url); 7 | 8 | const { authorization } = req.headers; 9 | if (!authorization) { 10 | res.status(401).send(); 11 | return; 12 | } 13 | 14 | try { 15 | const firebaseUser = await FirebaseWrapper.verifyIdToken(authorization); 16 | req.locals = { 17 | user: new User(firebaseUser), 18 | }; 19 | next(); 20 | } catch (e) { 21 | res.status(401).send(); 22 | return; 23 | } 24 | }; 25 | 26 | export default AuthFilter; 27 | -------------------------------------------------------------------------------- /packages/server/src/account/firebase-wrapper.ts: -------------------------------------------------------------------------------- 1 | import Firebase from 'firebase-admin'; 2 | 3 | export const initializeFirebase = (serviceAccountJson: any) => { 4 | Firebase.initializeApp({ 5 | credential: Firebase.credential.cert(serviceAccountJson), 6 | }); 7 | }; 8 | 9 | export interface FirebaseUser { 10 | user_id?: string; 11 | name?: string; 12 | email?: string; 13 | picture?: string; 14 | } 15 | 16 | class FirebaseWrapper { 17 | static async verifyIdToken(idToken: string) { 18 | const firebaseUser: FirebaseUser = await Firebase.auth().verifyIdToken(idToken); 19 | return firebaseUser; 20 | } 21 | } 22 | 23 | export default FirebaseWrapper; 24 | -------------------------------------------------------------------------------- /packages/server/src/account/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import UserController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const UserRouter = express.Router(); 6 | UserRouter.get('/', asyncRequestHandler(UserController.list)); 7 | 8 | UserRouter.get('/:uuid/extra', asyncRequestHandler(UserController.getExtra)); 9 | UserRouter.put('/:uuid/extra', asyncRequestHandler(UserController.updateExtra)); 10 | 11 | export default UserRouter; 12 | -------------------------------------------------------------------------------- /packages/server/src/account/user.ts: -------------------------------------------------------------------------------- 1 | import { FirebaseUser } from './firebase-wrapper'; 2 | import User from '../entity/user'; 3 | 4 | class LoggedUser { 5 | id: string; 6 | name?: string; 7 | email?: string; 8 | picture?: string; 9 | 10 | constructor(firebaseUser: FirebaseUser) { 11 | if (!firebaseUser.user_id) throw new Error('Firebase user without id'); 12 | 13 | this.id = firebaseUser.user_id; 14 | this.name = firebaseUser.name; 15 | this.email = firebaseUser.email; 16 | this.picture = firebaseUser.picture; 17 | } 18 | 19 | async getExtra() { 20 | const extra = await User.findOne(this.id); 21 | if (!extra) return User.create({ id: this.id }); 22 | return extra; 23 | } 24 | } 25 | 26 | export default LoggedUser; 27 | -------------------------------------------------------------------------------- /packages/server/src/application/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import ApplicationController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const ApplicationRouter = express.Router(); 6 | ApplicationRouter.get('/:uuid', asyncRequestHandler(ApplicationController.read)); 7 | ApplicationRouter.put('/:uuid', asyncRequestHandler(ApplicationController.update)); 8 | ApplicationRouter.get('/', asyncRequestHandler(ApplicationController.list)); 9 | ApplicationRouter.post('/', asyncRequestHandler(ApplicationController.create)); 10 | 11 | ApplicationRouter.post('/:uuid/deploy', asyncRequestHandler(ApplicationController.deploy)); 12 | ApplicationRouter.post('/import', asyncRequestHandler(ApplicationController.import)); 13 | 14 | export default ApplicationRouter; 15 | -------------------------------------------------------------------------------- /packages/server/src/base/error-filter.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import RequestError from './errors/request-error'; 3 | 4 | const ErrorFilter = (err: Error, _req: Request, res: Response, next: NextFunction) => { 5 | if (res.headersSent) { 6 | return next(err); 7 | } 8 | console.error(err); 9 | if (err instanceof RequestError) { 10 | res.status(err.statusCode).send(); 11 | return; 12 | } 13 | res.status(500); 14 | }; 15 | export default ErrorFilter; 16 | -------------------------------------------------------------------------------- /packages/server/src/base/errors/forbidden.ts: -------------------------------------------------------------------------------- 1 | import RequestError from './request-error'; 2 | 3 | class ForbiddenError extends RequestError { 4 | constructor() { 5 | super(403); 6 | } 7 | } 8 | 9 | export default ForbiddenError; 10 | -------------------------------------------------------------------------------- /packages/server/src/base/errors/not-found.ts: -------------------------------------------------------------------------------- 1 | import RequestError from './request-error'; 2 | 3 | class NotFoundError extends RequestError { 4 | constructor() { 5 | super(404); 6 | } 7 | } 8 | 9 | export default NotFoundError; 10 | -------------------------------------------------------------------------------- /packages/server/src/base/errors/request-error.ts: -------------------------------------------------------------------------------- 1 | class RequestError extends Error { 2 | constructor(public statusCode: number) { 3 | super('Request error'); 4 | } 5 | } 6 | 7 | export default RequestError; 8 | -------------------------------------------------------------------------------- /packages/server/src/base/router.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | export const asyncRequestHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => 4 | fn(req, res, next).catch(next); 5 | -------------------------------------------------------------------------------- /packages/server/src/dashboard/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | class DashboardController { 4 | public getDashboard = async (req: Request, res: Response) => { 5 | res.json({ 6 | applicationCount: 1, 7 | microfrontendCount: 5, 8 | }); 9 | }; 10 | } 11 | 12 | export default new DashboardController(); 13 | -------------------------------------------------------------------------------- /packages/server/src/dashboard/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import DashboardController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const DashboardRouter = express.Router(); 6 | DashboardRouter.get('/', asyncRequestHandler(DashboardController.getDashboard)); 7 | 8 | export default DashboardRouter; 9 | -------------------------------------------------------------------------------- /packages/server/src/database.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from 'typeorm'; 2 | 3 | const startDatabase = async (config: any) => { 4 | await createConnection({ 5 | ...config, 6 | type: 'postgres', 7 | entities: [__dirname + '/entity/*.js'], 8 | }); 9 | }; 10 | 11 | export default startDatabase; 12 | -------------------------------------------------------------------------------- /packages/server/src/entity/application.ts: -------------------------------------------------------------------------------- 1 | import { Entity, Column, BaseEntity, PrimaryGeneratedColumn } from 'typeorm'; 2 | import dayJs from 'dayjs'; 3 | 4 | import Microfrontend, { TYPE } from '../entity/microfrontend'; 5 | import Namespace from '../entity/namespace'; 6 | import { INTEGRATION_TYPE } from './integration/types'; 7 | 8 | interface IApplication { 9 | ownerId: string; 10 | name: string; 11 | packageName: string; 12 | slackChannelId?: string; 13 | integrationType?: INTEGRATION_TYPE; 14 | destinationId?: string; 15 | } 16 | 17 | @Entity() 18 | class Application extends BaseEntity { 19 | @PrimaryGeneratedColumn('uuid') 20 | public id?: string; 21 | 22 | @Column() 23 | public name: string = ''; 24 | 25 | @Column() 26 | public ownerId: string = ''; 27 | 28 | @Column() 29 | public createdAt: string = ''; 30 | 31 | @Column({ nullable: true }) 32 | public integrationType?: INTEGRATION_TYPE; 33 | 34 | @Column({ nullable: true }) 35 | public destinationId: string = ''; 36 | 37 | @Column({ nullable: true }) 38 | public slackChannelId: string = ''; 39 | 40 | static async createInstance(payload: IApplication) { 41 | const application = Application.create({ 42 | ...payload, 43 | createdAt: dayJs().format(), 44 | }); 45 | await application.save(); 46 | 47 | const containerMicrofrontend = await Microfrontend.createInstance({ 48 | name: `${payload.packageName} Container`, 49 | applicationId: application.id!, 50 | packageName: payload.packageName, 51 | ownerId: payload.ownerId, 52 | }); 53 | containerMicrofrontend.type = TYPE.CONTAINER; 54 | await containerMicrofrontend.save(); 55 | 56 | const mainNamespace = await Namespace.createInstance({ 57 | ownerId: payload.ownerId, 58 | name: 'Main namespace', 59 | path: '/', 60 | applicationId: application.id!, 61 | }); 62 | mainNamespace.isMain = true; 63 | await mainNamespace.save(); 64 | 65 | return application; 66 | } 67 | } 68 | 69 | export default Application; 70 | -------------------------------------------------------------------------------- /packages/server/src/entity/destination.ts: -------------------------------------------------------------------------------- 1 | import { Entity, BaseEntity, PrimaryColumn, Column } from 'typeorm'; 2 | 3 | export enum DESTINATION_TYPE { 4 | GITHUB_PAGES = 'GITHUB_PAGES', 5 | AWS_S3 = 'AWS_S3', 6 | } 7 | 8 | @Entity() 9 | class Destination extends BaseEntity { 10 | @PrimaryColumn('uuid') 11 | public id: string = ''; 12 | 13 | @Column() 14 | public type: DESTINATION_TYPE = DESTINATION_TYPE.AWS_S3; 15 | 16 | @Column('simple-json') 17 | public config: any = {}; 18 | } 19 | 20 | export default Destination; 21 | -------------------------------------------------------------------------------- /packages/server/src/entity/integration/base.ts: -------------------------------------------------------------------------------- 1 | export interface IIntegration { 2 | config: any; 3 | getArtifact(): void; 4 | publish(): void; 5 | listDestinationOptions(): Promise; 6 | listOriginOptions(): Promise; 7 | } 8 | 9 | abstract class Integration implements IIntegration { 10 | constructor(public config: any) {} 11 | 12 | public abstract getArtifact(): void; 13 | 14 | public abstract publish(): void; 15 | 16 | public abstract async listDestinationOptions(): Promise; 17 | 18 | public abstract async listOriginOptions(): Promise; 19 | } 20 | 21 | export default Integration; 22 | -------------------------------------------------------------------------------- /packages/server/src/entity/integration/github.ts: -------------------------------------------------------------------------------- 1 | import Integration from './base'; 2 | 3 | interface GithubConfig { 4 | token: string; 5 | } 6 | 7 | class GithubIntegration extends Integration { 8 | public listDestinationOptions(): Promise { 9 | throw new Error('Method not implemented.'); 10 | } 11 | public listOriginOptions(): Promise { 12 | throw new Error('Method not implemented.'); 13 | } 14 | async listOptions() { 15 | return ['asd']; 16 | } 17 | getArtifact() {} 18 | 19 | publish() {} 20 | } 21 | 22 | export default GithubIntegration; 23 | -------------------------------------------------------------------------------- /packages/server/src/entity/integration/types.ts: -------------------------------------------------------------------------------- 1 | import GithubIntegration from './github'; 2 | import AwsS3Integration from './aws-s3'; 3 | 4 | export enum INTEGRATION_TYPE { 5 | GITHUB = 'GITHUB', 6 | AWS_S3 = 'AWS_S3', 7 | } 8 | 9 | export const INTEGRATION_TYPE_CLASS = { 10 | [INTEGRATION_TYPE.GITHUB]: GithubIntegration, 11 | [INTEGRATION_TYPE.AWS_S3]: AwsS3Integration, 12 | }; 13 | -------------------------------------------------------------------------------- /packages/server/src/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryColumn, BaseEntity } from 'typeorm'; 2 | 3 | interface IUser { 4 | id: string; 5 | githubToken?: string; 6 | slackToken?: string; 7 | } 8 | 9 | @Entity() 10 | class User extends BaseEntity { 11 | @PrimaryColumn() 12 | public id: string = ''; 13 | 14 | @Column() 15 | public githubToken: string = ''; 16 | 17 | @Column() 18 | public slackToken: string = ''; 19 | 20 | static async createUser(payload: IUser) { 21 | const user = User.create({ 22 | ...payload, 23 | }); 24 | await user.save(); 25 | return user; 26 | } 27 | } 28 | 29 | export default User; 30 | -------------------------------------------------------------------------------- /packages/server/src/entity/version.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn, BaseEntity } from 'typeorm'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import dayJs from 'dayjs'; 4 | 5 | export enum STATUS { 6 | NEEDS_APROVAL = 'NEEDS_APROVAL', 7 | APPROVED = 'APPROVED', 8 | } 9 | 10 | interface IVersion { 11 | name: string; 12 | microfrontendId: string; 13 | sha?: string; 14 | path?: string; 15 | } 16 | 17 | @Entity() 18 | class Version extends BaseEntity { 19 | @PrimaryGeneratedColumn('uuid') 20 | public id: string = ''; 21 | 22 | @Column() 23 | public name: string = ''; 24 | 25 | @Column() 26 | public createdAt: string = ''; 27 | 28 | @Column() 29 | public microfrontendId?: string = ''; 30 | 31 | @Column() 32 | public sha: string = ''; 33 | 34 | @Column() 35 | public path: string = ''; 36 | 37 | @Column() 38 | public status: STATUS = STATUS.NEEDS_APROVAL; 39 | 40 | static build(payload: IVersion) { 41 | const version = Version.create({ 42 | ...payload, 43 | createdAt: dayJs().format(), 44 | id: uuidv4(), 45 | }); 46 | return version; 47 | } 48 | 49 | async approve() { 50 | this.status = STATUS.APPROVED; 51 | await this.save(); 52 | return this; 53 | } 54 | } 55 | export default Version; 56 | -------------------------------------------------------------------------------- /packages/server/src/external/list-folder.ts: -------------------------------------------------------------------------------- 1 | import fs, { copyFolder, rm, mkdir, writeJson, getDirsFrom, getAllFilesFromDir } from './utils/fs'; 2 | 3 | const getTreeFromFolder = async (folder: string) => { 4 | const a = await getAllFilesFromDir(folder); 5 | const tree = await Promise.all( 6 | a.map(async (filePath: string) => { 7 | const content = await fs.readFile(filePath); 8 | 9 | return { 10 | path: filePath.replace(`${folder}/`, ''), 11 | mode: '100644', 12 | type: 'blob', 13 | content: content.toString(), 14 | }; 15 | }) 16 | ); 17 | return tree; 18 | }; 19 | 20 | export default getTreeFromFolder; 21 | -------------------------------------------------------------------------------- /packages/server/src/external/path/path.test.ts: -------------------------------------------------------------------------------- 1 | // const metaTest = new PathTo('/tmp/123'); 2 | // console.info('metaTest.distFolder() ', metaTest.distFolder()); 3 | // console.info('metaTest.namespaceMetaJson() ', metaTest.namespaceMetaJson()); 4 | // console.info('metaTest.microVersionsFolder() ', metaTest.microVersionsFolder()); 5 | 6 | // const version = new Version(); 7 | // version.name = '1.1.0'; 8 | // const microfrontend = new Microfrontend(); 9 | // microfrontend.packageName = 'webapp'; 10 | // console.info('metaTest.microVersion({ version, microfrontend }) ', metaTest.microVersion({ version, microfrontend })); 11 | 12 | // let n = new Namespace(); 13 | // n.path = '/'; 14 | // n.isMain = true; 15 | // let pathToNamespace = metaTest.withNamespace(n); 16 | 17 | // console.info('MAIN pathToNamespace.root() ', pathToNamespace.root()); 18 | // console.info('MAIN pathToNamespace.microfrontendsFolder() ', pathToNamespace.microfrontendsFolder()); 19 | // console.info('MAIN pathToNamespace.microfrontendsMetaJson() ', pathToNamespace.microfrontendsMetaJson()); 20 | 21 | // n = new Namespace(); 22 | // n.path = '/beta'; 23 | // pathToNamespace = metaTest.withNamespace(n); 24 | 25 | // console.info('BETA pathToNamespace.root() ', pathToNamespace.root()); 26 | // console.info('BETA pathToNamespace.microfrontendsFolder() ', pathToNamespace.microfrontendsFolder()); 27 | // console.info('BETA pathToNamespace.microfrontendsMetaJson() ', pathToNamespace.microfrontendsMetaJson()); 28 | -------------------------------------------------------------------------------- /packages/server/src/github/octokat.ts: -------------------------------------------------------------------------------- 1 | import Octokat from 'octokat'; 2 | 3 | var defaults = { 4 | branchName: 'master', 5 | }; 6 | 7 | function init(options: any) { 8 | options = Object.assign({}, defaults, options); 9 | var head: any; 10 | 11 | var octo = new Octokat({ 12 | token: options.token, 13 | }); 14 | var repo = octo.repos(options.username, options.reponame); 15 | 16 | function fetchHead() { 17 | return repo.git.refs.heads(options.branchName).fetch(); 18 | } 19 | 20 | function fetchTree() { 21 | return fetchHead().then(function (commit: any) { 22 | head = commit; 23 | return repo.git.trees(commit.object.sha).fetch(); 24 | }); 25 | } 26 | 27 | function commit(files: any, message: any) { 28 | return Promise.all( 29 | files.map(function (file: any) { 30 | return repo.git.blobs.create({ 31 | content: file.content, 32 | encoding: 'utf-8', 33 | }); 34 | }) 35 | ) 36 | .then(function (blobs: any) { 37 | return fetchTree().then(function (tree: any) { 38 | return repo.git.trees.create({ 39 | tree: files.map(function (file: any, index: any) { 40 | return { 41 | path: file.path, 42 | mode: '100644', 43 | type: 'blob', 44 | sha: blobs[index].sha, 45 | }; 46 | }), 47 | basetree: tree.sha, 48 | }); 49 | }); 50 | }) 51 | .then(function (tree) { 52 | return repo.git.commits.create({ 53 | message: message, 54 | tree: tree.sha, 55 | parents: [head.object.sha], 56 | }); 57 | }) 58 | .then(function (commit) { 59 | return repo.git.refs.heads(options.branchName).update({ 60 | sha: commit.sha, 61 | }); 62 | }); 63 | } 64 | 65 | return { 66 | commit: commit, 67 | }; 68 | } 69 | 70 | export default init; 71 | -------------------------------------------------------------------------------- /packages/server/src/index.ts: -------------------------------------------------------------------------------- 1 | import App from './server'; 2 | import AWS from 'aws-sdk'; 3 | 4 | const { FIREBASE_ADMIN_CONFIG, FIREBASE_CONFIG, DATABASE_CONFIG, AWS_PROFILE, AWS_ARTIFACTS_BUCKET } = process.env; 5 | 6 | if (AWS_PROFILE) { 7 | console.info(`Credentials on AWS set to ${AWS_PROFILE}`); 8 | var credentials = new AWS.SharedIniFileCredentials({ profile: AWS_PROFILE }); 9 | AWS.config.credentials = credentials; 10 | } 11 | 12 | const configJson = { 13 | firebase: JSON.parse(FIREBASE_CONFIG!), 14 | firebaseAdmin: JSON.parse(FIREBASE_ADMIN_CONFIG!), 15 | database: JSON.parse(DATABASE_CONFIG!), 16 | baseUrl: '', 17 | }; 18 | 19 | const run = async () => { 20 | App.withDatabase(configJson.database) 21 | .withFirebaseConfig({ 22 | ...configJson.firebaseAdmin, 23 | private_key: JSON.parse(`"${configJson.firebaseAdmin.private_key}"`), 24 | }) 25 | .customizeApp((app: any) => { 26 | app.get('/customized', (_: any, res: any) => res.send('Custom hello world!')); 27 | }) 28 | .run(8080); 29 | }; 30 | 31 | run(); 32 | -------------------------------------------------------------------------------- /packages/server/src/integration/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { INTEGRATION_TYPE, INTEGRATION_TYPE_CLASS } from '../entity/integration/types'; 3 | 4 | class IntegrationController { 5 | public list = async (req: Request, res: Response) => { 6 | res.json(Object.keys(INTEGRATION_TYPE).map((integration) => ({ id: integration }))); 7 | }; 8 | 9 | public get = async (req: Request, res: Response) => { 10 | const integrationId = req.params.id; 11 | // const integration = INTEGRATION_TYPE_CLASS[integrationId]; 12 | res.json({ id: integrationId }); 13 | }; 14 | 15 | public listDestination = async (req: Request, res: Response) => { 16 | const integrationId = req.params.id; 17 | const IntegrationClazz = INTEGRATION_TYPE_CLASS[integrationId]; 18 | const integration = new IntegrationClazz({}); 19 | const list = await integration.listDestinationOptions(); 20 | res.json(list); 21 | }; 22 | 23 | public listOrigin = async (req: Request, res: Response) => { 24 | const integrationId = req.params.id; 25 | const IntegrationClazz = INTEGRATION_TYPE_CLASS[integrationId]; 26 | const integration = new IntegrationClazz({}); 27 | const list = await integration.listOriginOptions(); 28 | res.json(list); 29 | }; 30 | } 31 | 32 | export default new IntegrationController(); 33 | -------------------------------------------------------------------------------- /packages/server/src/integration/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import IntegrationController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const IntegrationRouter = express.Router(); 6 | IntegrationRouter.get('/', asyncRequestHandler(IntegrationController.list)); 7 | IntegrationRouter.get('/:id', asyncRequestHandler(IntegrationController.get)); 8 | IntegrationRouter.get('/:id/destination', asyncRequestHandler(IntegrationController.listDestination)); 9 | IntegrationRouter.get('/:id/origin', asyncRequestHandler(IntegrationController.listOrigin)); 10 | 11 | export default IntegrationRouter; 12 | -------------------------------------------------------------------------------- /packages/server/src/microfrontend/controller.ts: -------------------------------------------------------------------------------- 1 | import Microfrontend from '../entity/microfrontend'; 2 | import { getGithubRepository } from '../github/client'; 3 | import BaseController from '../base/controller'; 4 | import { INTEGRATION_TYPE } from '../entity/integration/types'; 5 | 6 | class MicrofrontendController extends BaseController { 7 | constructor() { 8 | super(Microfrontend); 9 | } 10 | 11 | list = this.createFilteredByList(['applicationId']); 12 | 13 | public syncVersions = this.withContext(async (req, res, context) => { 14 | const microfrontend = await context.getInstance(); 15 | await microfrontend.syncVersions(); 16 | res.json(microfrontend); 17 | }); 18 | 19 | public import = this.withContext(async (req, res, context) => { 20 | const user = await context.getUser(); 21 | const repository = await getGithubRepository(req.body.repositoryName); 22 | const application = await Microfrontend.createInstance({ 23 | ownerId: user.id, 24 | integrationType: INTEGRATION_TYPE.GITHUB, 25 | originId: repository.full_name, 26 | ...req.body, 27 | }); 28 | res.json(application); 29 | }); 30 | } 31 | 32 | export default new MicrofrontendController(); 33 | -------------------------------------------------------------------------------- /packages/server/src/microfrontend/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import MicrofrontendController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const MicrofrontendRouter = express.Router(); 6 | MicrofrontendRouter.get('/:uuid', asyncRequestHandler(MicrofrontendController.read)); 7 | MicrofrontendRouter.put('/:uuid', asyncRequestHandler(MicrofrontendController.update)); 8 | MicrofrontendRouter.get('/', asyncRequestHandler(MicrofrontendController.list)); 9 | MicrofrontendRouter.post('/', asyncRequestHandler(MicrofrontendController.create)); 10 | 11 | MicrofrontendRouter.post('/:uuid/sync', asyncRequestHandler(MicrofrontendController.syncVersions)); 12 | MicrofrontendRouter.post('/clear', asyncRequestHandler(MicrofrontendController.clear)); 13 | MicrofrontendRouter.post('/import', asyncRequestHandler(MicrofrontendController.import)); 14 | 15 | export default MicrofrontendRouter; 16 | -------------------------------------------------------------------------------- /packages/server/src/namespace/controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import Namespace from '../entity/namespace'; 3 | import BaseController from '../base/controller'; 4 | import Notification from '../notification/notification'; 5 | import Application from '../entity/application'; 6 | import Deploy from '../entity/deploy'; 7 | 8 | class NamespaceController extends BaseController { 9 | constructor() { 10 | super(Namespace); 11 | } 12 | 13 | public create = this.withContext(async (req, res, context) => { 14 | const user = await context.getUser(); 15 | const namespace = await Namespace.createInstance({ 16 | ownerId: user.id, 17 | ...req.body, 18 | }); 19 | res.json(namespace); 20 | }); 21 | 22 | public updateNextDeploy = this.withContext(async (req: Request, res: Response, context) => { 23 | const namespace = await context.getInstance(); 24 | const application = await Application.findOne(namespace.applicationId); 25 | const user = await context.getUser(); 26 | let nextDeploy = await namespace.getOrCreateNextDeploy(); 27 | nextDeploy = Deploy.merge(nextDeploy, req.body); 28 | await nextDeploy.save(); 29 | const userExtra = await user.getExtra(); 30 | 31 | Notification.sendChangeNextDeploy(userExtra, application!, namespace, nextDeploy); 32 | res.json(nextDeploy); 33 | 34 | return undefined; 35 | }); 36 | 37 | public getNextDeploy = this.createInstanceAction(async (namespace, req: Request, res: Response) => { 38 | const nextDeploy = await namespace.getNextDeploy(); 39 | res.json(nextDeploy); 40 | return undefined; 41 | }); 42 | 43 | public getHistory = this.createInstanceAction(async (namespace, req: Request, res: Response) => { 44 | const deploys = await namespace.getDeployHistory(); 45 | res.json({ 46 | deploys, 47 | }); 48 | return undefined; 49 | }); 50 | } 51 | 52 | export default new NamespaceController(); 53 | -------------------------------------------------------------------------------- /packages/server/src/namespace/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import NamespaceController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const NamespaceRouter = express.Router(); 6 | NamespaceRouter.get('/:uuid', asyncRequestHandler(NamespaceController.read)); 7 | NamespaceRouter.put('/:uuid', asyncRequestHandler(NamespaceController.update)); 8 | NamespaceRouter.delete('/:uuid', asyncRequestHandler(NamespaceController.delete)); 9 | NamespaceRouter.get('/', asyncRequestHandler(NamespaceController.list)); 10 | NamespaceRouter.post('/', asyncRequestHandler(NamespaceController.create)); 11 | 12 | NamespaceRouter.get('/:uuid/history', asyncRequestHandler(NamespaceController.getHistory)); 13 | NamespaceRouter.get('/:uuid/deploy/next', asyncRequestHandler(NamespaceController.getNextDeploy)); 14 | NamespaceRouter.put('/:uuid/deploy/next', asyncRequestHandler(NamespaceController.updateNextDeploy)); 15 | 16 | export default NamespaceRouter; 17 | -------------------------------------------------------------------------------- /packages/server/src/notification/integrations/slack.ts: -------------------------------------------------------------------------------- 1 | const { WebClient } = require('@slack/web-api'); 2 | 3 | class SlackMessage { 4 | static async sendSimple(token: string, channelId: string, text: string) { 5 | SlackMessage.send(token, channelId, [ 6 | { 7 | type: 'section', 8 | text: { 9 | type: 'mrkdwn', 10 | text: text, 11 | }, 12 | }, 13 | ]); 14 | } 15 | static async send(token: string, channelId: string, blocks: any[]) { 16 | const web = new WebClient(token); 17 | const res = await web.chat.postMessage({ 18 | channel: channelId, 19 | blocks, 20 | }); 21 | return res; 22 | } 23 | 24 | blocks: any[]; 25 | constructor(public token: string, public channelId: string) { 26 | this.blocks = []; 27 | } 28 | 29 | addText = (text: string) => { 30 | this.blocks.push({ 31 | type: 'section', 32 | text: { 33 | type: 'mrkdwn', 34 | text: text, 35 | }, 36 | }); 37 | return this; 38 | }; 39 | addSeparator = () => { 40 | this.blocks.push({ 41 | type: 'divider', 42 | }); 43 | return this; 44 | }; 45 | 46 | send = () => { 47 | SlackMessage.send(this.token, this.channelId, this.blocks); 48 | }; 49 | } 50 | 51 | export default SlackMessage; 52 | -------------------------------------------------------------------------------- /packages/server/src/version/controller.ts: -------------------------------------------------------------------------------- 1 | import BaseController from '../base/controller'; 2 | import { Request, Response } from 'express'; 3 | import Version from '../entity/version'; 4 | 5 | class VersionController extends BaseController { 6 | constructor() { 7 | super(Version); 8 | } 9 | 10 | list = this.createFilteredByList(['microfrontendId']); 11 | 12 | public approve = async (req: Request, res: Response) => { 13 | let version = await Version.findOne(req.params.uuid); 14 | if (!version) { 15 | res.status(404).send(); 16 | return; 17 | } 18 | 19 | version = await version.approve(); 20 | res.json(version); 21 | }; 22 | } 23 | 24 | export default new VersionController(); 25 | -------------------------------------------------------------------------------- /packages/server/src/version/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import VersionController from './controller'; 3 | import { asyncRequestHandler } from '../base/router'; 4 | 5 | const VersionRouter = express.Router(); 6 | VersionRouter.get('/:uuid', asyncRequestHandler(VersionController.read)); 7 | VersionRouter.get('/', asyncRequestHandler(VersionController.list)); 8 | 9 | VersionRouter.post('/clear', asyncRequestHandler(VersionController.clear)); 10 | VersionRouter.post('/:uuid/approve', asyncRequestHandler(VersionController.approve)); 11 | 12 | export default VersionRouter; 13 | -------------------------------------------------------------------------------- /packages/webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/webapp/lib/index.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | const exec = (command, { cwd, onStdout, onStderr, debug = true } = {}) => 4 | new Promise((resolve, reject) => { 5 | const spawnProcess = spawn(command, [], { shell: true, cwd }); 6 | 7 | if (onStdout || debug) spawnProcess.stdout.on('data', onStdout || ((data) => process.stdout.write(data))); 8 | if (onStderr || debug) spawnProcess.stderr.on('data', onStderr || ((data) => process.stderr.write(data))); 9 | 10 | spawnProcess.on('exit', (code) => { 11 | if (code > 0) { 12 | reject(code); 13 | return; 14 | } 15 | resolve(); 16 | }); 17 | }); 18 | 19 | module.exports = { 20 | build: async ({ env }) => { 21 | const envVars = Object.keys(env) 22 | .map((key) => `REACT_APP_${key}='${env[key]}'`) 23 | .join(' '); 24 | 25 | const command = `${envVars} npm run build`; 26 | await exec(command, { cwd: `${__dirname}/../` }); 27 | return `${__dirname}/../build`; 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /packages/webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cmra/webapp", 3 | "version": "1.0.3", 4 | "homepage": "./", 5 | "main": "./lib/index.js", 6 | "dependencies": { 7 | "@ant-design/icons": "^4.0.5", 8 | "@testing-library/jest-dom": "^4.2.4", 9 | "@testing-library/react": "^9.3.2", 10 | "@testing-library/user-event": "^7.1.2", 11 | "@types/jest": "^24.0.0", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^16.9.0", 14 | "@types/react-dom": "^16.9.0", 15 | "@types/react-router-dom": "^5.1.3", 16 | "antd": "^4.2.5", 17 | "axios": "^0.19.2", 18 | "axios-hooks": "^1.9.0", 19 | "firebase": "^7.14.5", 20 | "react": "^16.13.1", 21 | "react-dom": "^16.13.1", 22 | "react-firebase-hooks": "^2.2.0", 23 | "react-ga": "^2.7.0", 24 | "react-icons": "^3.9.0", 25 | "react-router-dom": "^5.1.2", 26 | "react-scripts": "3.4.1", 27 | "typescript": "~3.7.2" 28 | }, 29 | "scripts": { 30 | "start": "react-scripts start", 31 | "build": "react-scripts build", 32 | "test": "CI=true react-scripts test --passWithNoTests", 33 | "test:watch": "react-scripts test", 34 | "eject": "react-scripts eject", 35 | "deploy": "gh-pages -d build" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "gh-pages": "^2.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/webapp/public/favicon.ico -------------------------------------------------------------------------------- /packages/webapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Create micro react app 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/webapp/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matheusmr13/create-micro-react-app/f496b4a8f651db43f1f18a230ce4a3a1fe89e1d4/packages/webapp/public/logo.png -------------------------------------------------------------------------------- /packages/webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CMRA", 3 | "name": "Create Micro React App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#001629", 24 | "background_color": "#fff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/webapp/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/webapp/src/app/home/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | .App { 4 | display: flex; 5 | height: 100vh; 6 | } 7 | 8 | .App__container { 9 | flex-grow: 1; 10 | max-height: 100%; 11 | overflow: auto; 12 | } 13 | 14 | .App__menu-collapser { 15 | font-size: 18px; 16 | line-height: 64px; 17 | padding: 0 24px; 18 | cursor: pointer; 19 | transition: color 0.3s; 20 | } 21 | 22 | .App__menu-collapser:hover { 23 | color: #1890ff; 24 | } 25 | 26 | .App__logo-name { 27 | height: 32px; 28 | margin: 16px; 29 | font-size: 16px; 30 | color: #fff; 31 | font-weight: bold; 32 | text-align: center; 33 | display: flex; 34 | overflow: hidden; 35 | } 36 | 37 | .App__logo { 38 | height: 32px; 39 | margin: 0 8px; 40 | } 41 | 42 | .App__label { 43 | transition: opacity 0.3s; 44 | display: flex; 45 | align-items: center; 46 | padding-left: 8px; 47 | } 48 | 49 | .ant-layout-sider-collapsed .App__label { 50 | opacity: 0; 51 | } 52 | 53 | .App__header { 54 | background: #fff; 55 | padding: 0; 56 | display: flex; 57 | justify-content: space-between; 58 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 59 | } 60 | 61 | .App__header-avatar { 62 | cursor: pointer; 63 | } 64 | .App__header-actions { 65 | padding: 0 16px; 66 | display: flex; 67 | align-items: center; 68 | } 69 | 70 | .ant-layout-sider { 71 | box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); 72 | } 73 | 74 | .ant-layout-content { 75 | min-height: initial; 76 | } 77 | -------------------------------------------------------------------------------- /packages/webapp/src/app/home/dashboards.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | 5 | function Dashboards() { 6 | const [{ loading }] = useLoggedApiRequest(`/dashboards`); 7 | 8 | if (loading) return
loading
; 9 | 10 | return ( 11 |
12 | Home 13 |
14 | ); 15 | } 16 | 17 | export default Dashboards; 18 | -------------------------------------------------------------------------------- /packages/webapp/src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { HashRouter as Router } from 'react-router-dom'; 4 | 5 | import MainRouter from './router'; 6 | 7 | function App() { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /packages/webapp/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/webapp/src/base/components/page/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Row, Divider, Spin, PageHeader } from 'antd'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | import './page.css'; 7 | 8 | export interface IPageProps { 9 | title: string; 10 | subTitle?: string; 11 | children: React.ReactNode; 12 | actions?: Array; 13 | loading?: boolean; 14 | rootPage?: boolean; 15 | } 16 | 17 | export default function Page(props: IPageProps) { 18 | const history = useHistory(); 19 | const { title, children, actions, loading, subTitle, rootPage } = props; 20 | 21 | return ( 22 | history.goBack() })} 28 | > 29 | 30 | {loading ? ( 31 | 32 | 33 | 34 | ) : ( 35 | children 36 | )} 37 | 38 | ) 39 | } -------------------------------------------------------------------------------- /packages/webapp/src/base/components/page/page.css: -------------------------------------------------------------------------------- 1 | .page { 2 | background-color: #fff; 3 | } 4 | .page .ant-page-header-heading-title { 5 | font-size: 24px; 6 | } 7 | -------------------------------------------------------------------------------- /packages/webapp/src/base/components/section/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Typography, Row, Spin } from 'antd'; 4 | const { Title } = Typography; 5 | 6 | export interface ISectionProps { 7 | title: string; 8 | children: React.ReactNode; 9 | loading?: Boolean; 10 | } 11 | 12 | export default class Section extends React.Component { 13 | public render() { 14 | const { title, children, loading } = this.props; 15 | return ( 16 |
17 | {title} 18 | {loading ? ( 19 | 20 | 21 | 22 | ) : ( 23 | children 24 | )} 25 |
26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/webapp/src/base/hooks/local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | function useLocalStorage(key: string, initialValue?: string): [string | undefined, Function, Function] { 3 | const [storedValue, setStoredValue] = useState(() => { 4 | try { 5 | const item = JSON.parse(window.localStorage.getItem(key) || ''); 6 | return item ? item : initialValue; 7 | } catch (error) { 8 | return initialValue; 9 | } 10 | }); 11 | 12 | const setValue = (value: string) => { 13 | try { 14 | setStoredValue(value); 15 | window.localStorage.setItem(key, JSON.stringify(value)); 16 | } catch (error) { 17 | console.log(error); 18 | } 19 | }; 20 | 21 | const clearValue = () => { 22 | setStoredValue(undefined); 23 | window.localStorage.removeItem(key); 24 | }; 25 | 26 | return [storedValue, setValue, clearValue]; 27 | } 28 | 29 | export default useLocalStorage; 30 | -------------------------------------------------------------------------------- /packages/webapp/src/base/hooks/logged-user.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'modules/account/firebase'; 2 | import { useAuthState } from 'react-firebase-hooks/auth'; 3 | 4 | const useLoggedUser = () => { 5 | const [user] = useAuthState(firebase.auth()); 6 | if (!user) throw new Error('Cannot use this hook without being logged in'); 7 | return user; 8 | }; 9 | 10 | export default useLoggedUser; 11 | -------------------------------------------------------------------------------- /packages/webapp/src/base/hooks/query-param.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from 'react-router-dom'; 2 | 3 | function useQuery() { 4 | return new URLSearchParams(useLocation().search); 5 | } 6 | 7 | export default useQuery; 8 | -------------------------------------------------------------------------------- /packages/webapp/src/base/hooks/request.ts: -------------------------------------------------------------------------------- 1 | import { makeUseAxios } from 'axios-hooks'; 2 | 3 | import Axios from 'axios'; 4 | 5 | const BASE_URL = process.env.REACT_APP_BASE_URL; 6 | export const useApiRequest = makeUseAxios({ 7 | axios: Axios.create({ 8 | baseURL: BASE_URL, 9 | }), 10 | }); 11 | 12 | export const useLoggedApiRequest = makeUseAxios({ 13 | axios: null, 14 | }); 15 | 16 | const configureLoggedApiRequest = (token: any) => { 17 | if (!token) { 18 | useLoggedApiRequest.configure({ axios: null }); 19 | return; 20 | } 21 | const axios = Axios.create({ 22 | baseURL: BASE_URL, 23 | headers: { 24 | Authorization: token, 25 | }, 26 | }); 27 | axios.interceptors.response.use( 28 | (res) => { 29 | return res; 30 | }, 31 | (error) => { 32 | const { response } = error; 33 | if (response && response.status === 401) { 34 | localStorage.removeItem('auth'); 35 | window.location.reload(); 36 | return; 37 | } 38 | return Promise.reject(error); 39 | } 40 | ); 41 | useLoggedApiRequest.configure({ axios }); 42 | }; 43 | 44 | export const useGithubApiRequest = makeUseAxios({ 45 | axios: null, 46 | }); 47 | 48 | const configureGithubApiRequest = (token: string) => { 49 | useGithubApiRequest.configure({ 50 | axios: Axios.create({ 51 | baseURL: 'https://api.github.com', 52 | headers: { 53 | Authorization: `Bearer ${token}`, 54 | }, 55 | }), 56 | }); 57 | }; 58 | 59 | export const configureLoggedUser = (loggedUser: any) => { 60 | configureLoggedApiRequest(loggedUser.accessToken); 61 | 62 | // loggedUser.getIdToken().then((asd: string) => { 63 | // configureGithubApiRequest(loggedUser.accessToken); 64 | // }); 65 | }; 66 | 67 | export { default as useApiAction } from './api-action'; 68 | -------------------------------------------------------------------------------- /packages/webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | ReactDOM.render( 6 | // 7 | , 8 | // , 9 | document.getElementById('root') 10 | ); 11 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/account/firebase.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | const firebaseConfig = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG || ''); 4 | firebase.initializeApp(firebaseConfig); 5 | 6 | export default firebase; 7 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/account/login.css: -------------------------------------------------------------------------------- 1 | .Login { 2 | height: 100vh; 3 | width: 100vw; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | background-color: lightgray; 8 | } 9 | .Login__login-panel { 10 | background-color: white; 11 | padding: 36px; 12 | box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 13 | } 14 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/account/login.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Button, Space } from 'antd'; 4 | import { GithubFilled, GoogleCircleFilled } from '@ant-design/icons'; 5 | import './login.css'; 6 | import { message } from 'antd'; 7 | import firebase from './firebase'; 8 | 9 | function Login() { 10 | const handleLogin = (provider: any) => async () => { 11 | try { 12 | await firebase.auth().signInWithPopup(provider); 13 | } catch (e) { 14 | console.error(e); 15 | message.error('Something went wrong!'); 16 | } 17 | } 18 | 19 | const handleGoogleLogin = handleLogin(new firebase.auth.GoogleAuthProvider()); 20 | return ( 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | ); 29 | } 30 | 31 | export default Login; 32 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/account/profile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLoggedApiRequest } from 'base/hooks/request'; 3 | import useApiAction from 'base/hooks/api-action'; 4 | import useLoggedUser from 'base/hooks/logged-user'; 5 | import Page from 'base/components/page'; 6 | 7 | import { Form, Input, Button, Space, Spin } from 'antd'; 8 | 9 | function Profiile() { 10 | const user = useLoggedUser(); 11 | const [{ data: profile, loading }] = useLoggedApiRequest(`/users/${user.uid}/extra`); 12 | const [{ loading: savingProfile }, saveProfile] = useApiAction(`/users/${user.uid}/extra`, { 13 | method: 'put', 14 | message: { 15 | success: 'User saved', 16 | }, 17 | }); 18 | 19 | const onFinish = async (data: any) => { 20 | await saveProfile({ 21 | data, 22 | }); 23 | }; 24 | 25 | return ( 26 | 27 | {loading ? ( 28 | 29 | ) : ( 30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 |
47 | )} 48 |
49 | ); 50 | } 51 | 52 | export default Profiile; 53 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/details/microfrontend-list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Card, Col, Row, Tag, Space } from 'antd'; 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { Link } from 'react-router-dom'; 5 | import Section from 'base/components/section'; 6 | 7 | interface IMicrofrontendListProps { 8 | applicationId: string; 9 | } 10 | 11 | const MicrofrontendList: React.FunctionComponent = ({ applicationId }) => { 12 | const [{ data: microfrontends, loading }, refetch] = useLoggedApiRequest(`/microfrontends?applicationId=${applicationId}`); 13 | 14 | useEffect(() => { 15 | refetch(); 16 | }, [refetch]); 17 | return ( 18 |
19 | 20 | {microfrontends && 21 | microfrontends.map((microfrontend: any) => ( 22 | 23 | Edit}> 24 | 25 | package: {microfrontend.packageName} 26 | type: {microfrontend.type} 27 | 28 | 29 | 30 | ))} 31 | 32 |
33 | ); 34 | }; 35 | 36 | export default MicrofrontendList; 37 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/details/namespace-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Card, Col, Row } from 'antd'; 3 | import { Link } from 'react-router-dom'; 4 | import Section from 'base/components/section'; 5 | 6 | interface INamespaceListProps { 7 | namespaces: any[]; 8 | } 9 | 10 | const NamespaceList: React.FunctionComponent = ({ namespaces }) => { 11 | return ( 12 |
13 | 14 | {namespaces && 15 | namespaces.map((microfrontend: any) => ( 16 | 17 | Edit}> 18 | {microfrontend.name} 19 | 20 | 21 | ))} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default NamespaceList; 28 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/fetch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { useParams } from 'react-router-dom'; 5 | import Page from 'base/components/page'; 6 | 7 | function FetchApplication(props: { children: Function; title: string; applicationId?: string }) { 8 | const { children, title, applicationId: applicationIdProp } = props; 9 | const { applicationId = applicationIdProp } = useParams(); 10 | const [{ data: application, loading: loadingApplication }, refetch] = useLoggedApiRequest( 11 | `/applications/${applicationId}` 12 | ); 13 | 14 | useEffect(() => { 15 | refetch(); 16 | }, [refetch]); 17 | 18 | return ( 19 | 20 | {!loadingApplication && children(application)} 21 | 22 | ); 23 | } 24 | 25 | export default FetchApplication; 26 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 3 | 4 | import Details from './details'; 5 | import List from './list'; 6 | import New from './new'; 7 | 8 | function ApplicationHome() { 9 | let match = useRouteMatch(); 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default ApplicationHome; 27 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useLoggedApiRequest } from 'base/hooks/request'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | 6 | import Page from 'base/components/page'; 7 | import { List, Button } from 'antd'; 8 | 9 | function ApplicationList() { 10 | const [{ data: applications }, refetch] = useLoggedApiRequest('/applications', { manual: true }); 11 | 12 | useEffect(() => { 13 | refetch(); 14 | }, [refetch]); 15 | 16 | if (!applications) return null; 17 | return ( 18 | ]} 22 | > 23 | ( 28 | Edit} 31 | > 32 | 36 | 37 | )} 38 | /> 39 | 40 | ); 41 | } 42 | 43 | export default ApplicationList; 44 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/application/new.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Redirect } from 'react-router-dom'; 4 | import { Form, Input, Button } from 'antd'; 5 | 6 | import useApiAction from 'base/hooks/api-action'; 7 | import Page from 'base/components/page'; 8 | 9 | const ApplicationDetails: React.FunctionComponent = () => { 10 | const [{ data, error, loading: creatingApplication }, createApplication] = useApiAction('/applications', { 11 | method: 'POST', 12 | message: { 13 | success: 'Application created!', 14 | }, 15 | }); 16 | 17 | const onFinish = async (data: any) => { 18 | await createApplication({ data }); 19 | }; 20 | 21 | if (creatingApplication) return null; 22 | 23 | if (!creatingApplication && data && !error) return ; 24 | 25 | return ( 26 | 27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default ApplicationDetails; 46 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/github/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 3 | 4 | import Import from './import'; 5 | import Repositories from './repositories'; 6 | 7 | function GithubHome() { 8 | let match = useRouteMatch(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default GithubHome; 23 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/microfrontend/details.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { useParams } from 'react-router-dom'; 5 | 6 | import MicrofrontendForm from './form'; 7 | 8 | function MicrofrontendDetails() { 9 | let { microfrontendId } = useParams(); 10 | const [{ data: microfrontend, loading }] = useLoggedApiRequest(`/microfrontends/${microfrontendId}`); 11 | 12 | if (loading) return
loading
; 13 | 14 | return ; 15 | } 16 | 17 | export default MicrofrontendDetails; 18 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/microfrontend/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 4 | 5 | import Details from './details'; 6 | import New from './new'; 7 | import List from './list'; 8 | 9 | function MicrofrontendsHome() { 10 | let match = useRouteMatch(); 11 | 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | } 28 | 29 | export default MicrofrontendsHome; 30 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/microfrontend/list.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useLoggedApiRequest } from 'base/hooks/request'; 3 | 4 | import { Link } from 'react-router-dom'; 5 | 6 | import Page from 'base/components/page'; 7 | import { List } from 'antd'; 8 | 9 | function MicrofrontendList() { 10 | const [{ data: microfrontends }, refetch] = useLoggedApiRequest('/microfrontends', { manual: true }); 11 | 12 | useEffect(() => { 13 | refetch(); 14 | }, [refetch]); 15 | 16 | if (!microfrontends) return null; 17 | 18 | return ( 19 | 20 | ( 25 | Edit} 28 | > 29 | 30 | 31 | )} 32 | /> 33 | 34 | ); 35 | } 36 | 37 | export default MicrofrontendList; 38 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/microfrontend/new.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MicrofrontendForm from './form'; 3 | 4 | function NewMicrofrontend() { 5 | return ; 6 | } 7 | 8 | export default NewMicrofrontend; 9 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/namespace/details/deploy-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Timeline } from 'antd'; 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import Section from 'base/components/section'; 5 | 6 | interface IDeployListProps { 7 | namespaceId: string; 8 | } 9 | 10 | const DeployList: React.FunctionComponent = ({ namespaceId }) => { 11 | const [{ data: history, loading }] = useLoggedApiRequest(`/namespaces/${namespaceId}/history`); 12 | 13 | return ( 14 |
15 | 16 | {history && 17 | history.deploys.map((deploy: any) => ( 18 | 19 | Deploy {deploy.id} created at {deploy.createdAt} 20 | 21 | ))} 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default DeployList; 28 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/namespace/details/namespace-list.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Card, Col, Row, Typography } from 'antd'; 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { Link } from 'react-router-dom'; 5 | import Section from 'base/components/section'; 6 | 7 | interface INamespaceListProps { 8 | applicationId: string; 9 | } 10 | 11 | const NamespaceList: React.FunctionComponent = ({ applicationId }) => { 12 | const [{ data: namespaces, loading }] = useLoggedApiRequest(`/namespaces?applicationId=${applicationId}`); 13 | return ( 14 |
15 | 16 | {namespaces && 17 | namespaces.map((microfrontend: any) => ( 18 | 19 | Edit}> 20 | {microfrontend.name} 21 | 22 | 23 | ))} 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default NamespaceList; 30 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/namespace/fetch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { useParams } from 'react-router-dom'; 5 | import Page from 'base/components/page'; 6 | 7 | function FetchNamespace(props: { children: Function; title?: string; namespaceId?: string }) { 8 | const { children, title, namespaceId: namespaceIdProp } = props; 9 | const { namespaceId = namespaceIdProp } = useParams(); 10 | const [{ data: namespace, loading: loadingNamespace }] = useLoggedApiRequest( 11 | `/namespaces/${namespaceId}` 12 | ); 13 | 14 | if (loadingNamespace) return null; 15 | 16 | if (!title) return children(namespace); 17 | 18 | return ( 19 | 20 | {!loadingNamespace && children(namespace)} 21 | 22 | ); 23 | } 24 | 25 | export default FetchNamespace; 26 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/namespace/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 3 | 4 | import { Details, New } from './details'; 5 | import Deploy from './deploy'; 6 | 7 | function ApplicationHome() { 8 | let match = useRouteMatch(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | ); 23 | } 24 | 25 | export default ApplicationHome; 26 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/version/details.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useLoggedApiRequest } from 'base/hooks/request'; 4 | import { useParams } from 'react-router-dom'; 5 | 6 | import VersionForm from './form'; 7 | 8 | function VersionDetails() { 9 | let { versionId } = useParams(); 10 | const [{ data: version, loading }] = useLoggedApiRequest(`/versions/${versionId}`); 11 | 12 | if (loading) return
loading
; 13 | 14 | return ; 15 | } 16 | 17 | export default VersionDetails; 18 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/version/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Switch, Route, useRouteMatch } from 'react-router-dom'; 4 | 5 | import Details from './details'; 6 | import New from './new'; 7 | 8 | function VersionsHome() { 9 | let match = useRouteMatch(); 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | {/* */} 21 | 22 |
23 | ); 24 | } 25 | 26 | export default VersionsHome; 27 | -------------------------------------------------------------------------------- /packages/webapp/src/modules/version/new.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import VersionForm from './form'; 3 | 4 | function NewVersion() { 5 | return ; 6 | } 7 | 8 | export default NewVersion; 9 | -------------------------------------------------------------------------------- /packages/webapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/webapp/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /packages/webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "baseUrl": "./src" 18 | }, 19 | "include": ["src", "lib"] 20 | } 21 | -------------------------------------------------------------------------------- /presentation_guide.md: -------------------------------------------------------------------------------- 1 | # Microfrontend world 2 | 3 | ## Basic creation 4 | 5 | Create a repository on github and name it "my-microfrontend-market" 6 | Clone it 7 | `@cmra/cli create -am` 8 | push && build && publish it 9 | 10 | ## Marketplace 11 | 12 | Login with github 13 | Generate github developer key 14 | Go to profile and save it 15 | Import application 16 | Import microfrontend 17 | Deploy application && show changes 18 | 19 | ## Simplicity to add new microfrontend 20 | 21 | Create new repo 22 | Clone it 23 | `@cmra/cli create -m` 24 | push && build && publish it 25 | Import microfrontend 26 | Deploy application && show changes 27 | 28 | ## Namespaces 29 | 30 | Change some substancial stuff from first microfrontend (background color? :D) 31 | push && build && publish it 32 | Create a namespace named BETA 33 | Choose new microfrontend version for new namespace 34 | Deploy application 35 | Deploy application && show changes 36 | 37 | ## Example application 38 | 39 | Show application 40 | 41 | - Cart -> Has state Cart with itens 42 | - Showcase -> Add item to cart -> Has state with filter by 43 | - Promotions -> List some promotions that filters itens on Showcase 44 | - Design System -> Responsible for all buttons on the page 45 | 46 | Design System -> Version 1 (black and white, hard corners, monospaced) | Version 2 (colorfull, rounded, comic sans) 47 | -------------------------------------------------------------------------------- /scripts/publish-site.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const ghPages = require('gh-pages'); 4 | const { exec } = require('./utils'); 5 | 6 | // require('dotenv').config({ path: '.env.local' }); 7 | 8 | // const { GITHUB_CLIENT_ID } = process.env; 9 | const deployFrontend = async () => { 10 | try { 11 | await exec('npm run build', { cwd: './packages/doc', debug: true }); 12 | 13 | await new Promise((resolve, reject) => { 14 | ghPages.publish('./packages/doc/build', {}, (error) => { 15 | if (error) { 16 | console.error(error); 17 | reject(error); 18 | return; 19 | } 20 | resolve(); 21 | }); 22 | }); 23 | } catch (e) { 24 | console.info(e); 25 | } 26 | }; 27 | 28 | deployFrontend(); 29 | -------------------------------------------------------------------------------- /scripts/recreate-lock.sh: -------------------------------------------------------------------------------- 1 | root_dir=$(pwd) 2 | set -e 3 | 4 | debug () { 5 | echo "====================================" 6 | echo "====================================" 7 | echo "$1 $(pwd)" 8 | echo "====================================" 9 | echo "====================================" 10 | } 11 | 12 | delete_yarn_lock() { 13 | debug "delete_yarn_lock" 14 | rm -rf yarn.lock 2> /dev/null 15 | } 16 | 17 | link_example_deps() { 18 | debug "link_example_deps" 19 | cd node_modules 20 | echo "removing deps" 21 | 22 | rm -rf @cmra/cli 2> /dev/null 23 | rm -rf @cmra/react 2> /dev/null 24 | rm -rf react 2> /dev/null 25 | 26 | echo "linking again" 27 | ln -s "${root_dir}/packages/@cmra/cli/" @cmra/cli 28 | ln -s "${root_dir}/packages/@cmra/react/" @cmra/react 29 | ln -s "${root_dir}/packages/@cmra/react/node_modules/react" react 30 | echo "linking done" 31 | 32 | cd .. 33 | } 34 | 35 | setup_examples() { 36 | debug "setup_examples" 37 | cd ./examples 38 | examples=$(ls .) 39 | for D in $examples 40 | do 41 | cd $D 42 | recreate "examples" 43 | cd .. 44 | done 45 | cd .. 46 | } 47 | 48 | recreate() { 49 | debug "recreate" 50 | 51 | lerna clean -y 52 | 53 | cd ./packages 54 | packages=$(ls .) 55 | for D in $packages 56 | do 57 | cd $D 58 | delete_yarn_lock 59 | cd .. 60 | done 61 | 62 | cd .. 63 | 64 | rm -rf node_modules 2> /dev/null 65 | rm -rf yarn.lock 2> /dev/null 66 | lerna bootstrap 67 | yarn 68 | 69 | if [[ $1 = "examples" ]]; then 70 | 71 | echo "to aqui antes do link_example_deps $(pwd)" 72 | link_example_deps 73 | echo "to aqui depois do link $(pwd)" 74 | cd ./packages 75 | 76 | for D in $packages 77 | do 78 | cd $D 79 | link_example_deps 80 | cd .. 81 | done 82 | 83 | cd .. 84 | fi 85 | } 86 | 87 | # recreate 88 | setup_examples 89 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const dotEnv = require('dotenv'); 4 | const { exec } = require('./utils'); 5 | 6 | const { parsed: newEnvVars } = dotEnv.config({ path: '.env.development.local' }); 7 | 8 | const run = async () => { 9 | const envVars = Object.keys(newEnvVars).map((key) => `${key.toUpperCase()}='${newEnvVars[key]}'`); 10 | 11 | const backendEnv = envVars.join(' '); 12 | const frontendEnv = envVars.map((env) => `REACT_APP_${env}`).join(' '); 13 | 14 | exec(`${frontendEnv} REACT_APP_BASE_URL=http://localhost:8080 PORT=3333 npm start`, { 15 | cwd: './packages/webapp', 16 | }); 17 | 18 | exec(`${backendEnv} npm run start:watch`, { 19 | cwd: './packages/server', 20 | }); 21 | }; 22 | 23 | run(); 24 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | const exec = (command, { cwd, onStdout, onStderr, debug = true } = {}) => 4 | new Promise((resolve, reject) => { 5 | const spawnProcess = spawn(command, [], { shell: true, cwd }); 6 | 7 | if (onStdout || debug) spawnProcess.stdout.on('data', onStdout || ((data) => process.stdout.write(data))); 8 | if (onStderr || debug) spawnProcess.stderr.on('data', onStderr || ((data) => process.stderr.write(data))); 9 | 10 | spawnProcess.on('exit', (code) => { 11 | if (code > 0) { 12 | reject(code); 13 | return; 14 | } 15 | resolve(); 16 | }); 17 | }); 18 | 19 | module.exports = { 20 | exec, 21 | }; 22 | --------------------------------------------------------------------------------