├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── development.env ├── nest-cli.json ├── nodemon-debug.json ├── nodemon.json ├── package.json ├── src ├── app.module.ts ├── interfaces │ ├── bundle.interface.ts │ ├── file-accessor.interface.ts │ └── manifest.interface.ts ├── main.ts ├── modules │ ├── config │ │ ├── config.module.ts │ │ └── config.service.ts │ └── registry │ │ ├── registry.controller.ts │ │ ├── registry.module.ts │ │ └── registry.service.ts ├── tasks │ ├── css.tasks.ts │ └── task.module.ts ├── templates │ ├── app.wrapper.template.js │ ├── global-inject.template.js │ ├── service.wrapper.template.js │ └── web-component.wrapper.template.js └── untilities │ ├── css.utils.ts │ ├── file.utils.ts │ ├── html.utils.ts │ ├── js.utils.ts │ ├── remote-file.utils.ts │ ├── template.utils.ts │ └── utility.module.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | src/micro-app-registry/**/* 3 | .idea/**/* 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": true, 9 | "printWidth": 120 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Öner Zafer 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # _microfe-registry_ 2 | 3 | _(microfe - short for micro frontends)_ 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ npm install 9 | mkdir micro-fe-registry 10 | ``` 11 | 12 | Then do the following for each app you would like to have in your micro-app tree 13 | 14 | ```bash 15 | cp path/toyour/app/dist/folder micro-fe-registry/yourAppNameCamelCase 16 | touch micro-fe-registry/yourAppNameCamelCase/micor-fe-manifest.json 17 | ``` 18 | 19 | For each micro-fe-registry.json file please refer the templates provided under [Microfe Manifest](#microfe-manifest) 20 | 21 | ## Helpful commands 22 | 23 | ### Running the app 24 | 25 | ```bash 26 | # development 27 | $ npm run start 28 | 29 | # watch mode 30 | $ npm run start:dev 31 | 32 | # incremental rebuild (webpack) 33 | $ npm run webpack 34 | $ npm run start:hmr 35 | 36 | # production mode 37 | $ npm run start:prod 38 | ``` 39 | 40 | ### Test 41 | 42 | ```bash 43 | # unit tests 44 | $ npm run test 45 | 46 | # e2e tests 47 | $ npm run test:e2e 48 | 49 | # test coverage 50 | $ npm run test:cov 51 | ``` 52 | 53 | ## Developing a Micro-app 54 | 55 | microfe address some possible problems with some set of built-in solutions. Isolated app scope is solved by wrapping 56 | the apps into web components. 57 | 58 | - For the app to app communication MicroAppStore is provided. 59 | - To share a global set of configuration Config is provided. 60 | - For async dependencies, AppsManager is the main app loader and provider 61 | All these solutions and more are provided by microfe as globals relative to micro-apps. Here these micro-app relative 62 | globals will be mentioned as Runtime Relative Globals. 63 | 64 | ## Runtime _Relative_ Global Variables 65 | 66 | All micro-apps under micro-fe-registry will be wrapped into some templates just to be ready for consumption. Templates 67 | are designed to provide a unified interface for each app and also scoping. According to a micro-app, defined variables 68 | within this scope are global variables. 69 | 70 | ```TypeScript 71 | interface MicroAppArgs { 72 | AppsManager: AppsManager; 73 | Config: ConfigInterface; 74 | conatiner: HTMLElement; 75 | [key: string]: any 76 | } 77 | // RELATIVE GLOBALS 78 | const microAppArgs: MicroAppArgs; 79 | const CONTAINER: HTMLElement; 80 | const DOCUMENT: Document; 81 | const WINDOW: Window; 82 | ``` 83 | 84 | All the declared dependencies will be provided under microAppArgs with given name as the key of the instance of the app. 85 | 86 | ### Microfe Manifest 87 | 88 | Microfe-registry assumes a couple of different scenarios for each possible app and it is subject to changes in the 89 | future. Supported and tested projects can be listed as below: 90 | 91 | - Static HTML, CSS and js interactions pages (no jquery support so far) 92 | - Single page apps build with major frameworks 93 | - JS apps with no UI 94 | 95 | ```JSON 96 | { 97 | "name": "AppName", 98 | "version": "1.0.0", 99 | "type": "web component", 100 | "bundle": [ 101 | {"type": "template", "path": "template.html"}, 102 | {"type": "html", "path": "index.html"}, 103 | {"type": "js", "path": "js/script.js"}, 104 | {"type": "css", "path": "css/styles.css"} 105 | ], 106 | "globalBundle": [ 107 | {"type": "js", "path": "js/global.script.js"}, 108 | {"type": "css", "path": "css/global.styles.css"} 109 | ], 110 | "dependencies": { 111 | "OtherApp": "1.0.0", 112 | "SomeOtherApp": "1.0.0" 113 | } 114 | } 115 | ``` 116 | 117 | The example above contains all possible values for a manifest file, but under normal conditions, you won't need all of 118 | them at the same time. 119 | 120 | - **type**: _'web component' | 'webcomponent'_ use the type if and only if you have a web component or series of web 121 | components as your app, otherwise it is not recommended to use. 122 | - **bundle**: _{type: "template" | "html" | "js" | "css", path: string}[]_ at least one item under bundle is required. 123 | Under normal conditions, index.html has all the necessary paths for CSS and js and inline CSS and js, so if you 124 | provide the only index.html as a bundle all link and script and style tags will be included to your micro-app. 125 | For each bundle item, you have to define a **type** and a **path**. If you define more than one template or HTML 126 | bundle, the only first one will be provided under your app. 127 | - **globalBundle**: _{type: "js" | "css", path: string}[]_ Sometimes we need to inject something to global for 128 | instance @font-face is not supported under shadowDom so we need to inject it to global. For that kind of needs, 129 | globalBundle is provided. Use it wisely :) 130 | - **dependencies**: _{[key: string]: string}_ Within this property you may define all required micro-apps with their 131 | relevant versions. Currently, versions are not supported but will be in supported soon. 132 | 133 | ### Platform Specific Tips 134 | 135 | #### Static SPA 136 | 137 | Just copy files as is under micro-fe-registry folder and add necessary micro-fe-manifest.json and the app is ready to 138 | run. Just notice if your spa is based on jquery, unfortunately, it will not work as micro-app because of non-supported 139 | shadow dom usage. 140 | 141 | #### React 142 | 143 | If you are using create-react-app please refer its documentation for building the app for production. As a limitation 144 | a micro-app has written in react please follow the instruction for static assets 145 | 146 | ```jsx harmony 147 | // following implementation will have broken image as micro-app 148 | import * as Logo from '.logo.svg'; 149 | const App = () => ; 150 | ``` 151 | 152 | Instead of the example above prefer the following one: 153 | 154 | ```jsx harmony 155 | // after moving static asset to public folder or a cdn following example will work as expected 156 | const MicroApp = () => ; 157 | ``` 158 | 159 | Then build your application and copy the dist folder as YourAppName under micro-fe-registry folder under the src folder. 160 | After adding micro-fe-manifest.json the micro-app will work as expected. 161 | 162 | #### Angular 163 | 164 | The tricky one is Angular apps. An angular app should have Angular 6+ version to be built as a web component. Since 165 | the mounting mechanism of angular is a little bit different than most of the popular frameworks it needs to be built 166 | as a web component. The recommended tag name for your angular web component is your-app-name-container. When 167 | microfe-registry wrapping angular app it exposes another web component with the tag your-app-name. By that way, 168 | conflicting web component definition will be prevented. How to bundle an angular app as a web component can be 169 | found under angular elements documentation. The rest is built the app for production, copy files under 170 | micro-fe-registry folder and create the micro-fe-manifest.json file. 171 | 172 | # License 173 | 174 | [MIT](https://choosealicense.com/licenses/mit/) 175 | -------------------------------------------------------------------------------- /development.env: -------------------------------------------------------------------------------- 1 | DOMAIN=http://localhost:3000 2 | REGISTRY_PATH=micro-app-registry 3 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /nodemon-debug.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "node --inspect-brk -r ts-node/register -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "micro-fe-registry", 3 | "version": "0.0.0", 4 | "description": "a registry server fro micro-fe loader", 5 | "author": "öner zafer", 6 | "license": "MIT", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.build.json", 9 | "format": "prettier --write \"src/**/*.ts\"", 10 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "start:dev": "nodemon", 12 | "start:debug": "nodemon --config nodemon-debug.json", 13 | "prestart:prod": "rimraf dist && tsc", 14 | "start:prod": "node dist/main.js", 15 | "lint": "tslint -p tsconfig.json -c tslint.json", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@angular/cli": "^7.2.2", 24 | "@nestjs/common": "^5.4.0", 25 | "@nestjs/core": "^5.4.0", 26 | "cheerio": "^1.0.0-rc.2", 27 | "dashify": "^2.0.0", 28 | "dotenv": "^6.2.0", 29 | "global": "^4.3.2", 30 | "nest-schedule": "^0.4.5", 31 | "reflect-metadata": "^0.1.12", 32 | "rimraf": "^2.6.2", 33 | "rxjs": "^6.2.2", 34 | "strip-comments": "^1.0.2", 35 | "typescript": "^3.0.1", 36 | "uniqid": "^5.0.3", 37 | "walk": "^2.3.14" 38 | }, 39 | "devDependencies": { 40 | "@nestjs/testing": "^5.1.0", 41 | "@types/express": "^4.16.0", 42 | "@types/jest": "^23.3.1", 43 | "@types/node": "^10.7.1", 44 | "@types/supertest": "^2.0.5", 45 | "jest": "^23.5.0", 46 | "nodemon": "^1.18.3", 47 | "prettier": "^1.14.2", 48 | "supertest": "^3.1.0", 49 | "ts-jest": "^23.1.3", 50 | "ts-loader": "^4.4.2", 51 | "ts-node": "^7.0.1", 52 | "tsconfig-paths": "^3.5.0", 53 | "tslint": "5.11.0" 54 | }, 55 | "jest": { 56 | "moduleFileExtensions": [ 57 | "js", 58 | "json", 59 | "ts" 60 | ], 61 | "rootDir": "src", 62 | "testRegex": ".spec.ts$", 63 | "transform": { 64 | "^.+\\.(t|j)s$": "ts-jest" 65 | }, 66 | "coverageDirectory": "../coverage", 67 | "testEnvironment": "node" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RegistryModule } from './modules/registry/registry.module'; 3 | import { TaskModule } from './tasks/task.module'; 4 | 5 | @Module({ 6 | imports: [RegistryModule, TaskModule], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /src/interfaces/bundle.interface.ts: -------------------------------------------------------------------------------- 1 | export interface BundleItem { 2 | type: 'css' | 'js' | 'template' | 'html'; 3 | path: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/file-accessor.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FileAccessor { 2 | readFile(path: string): Promise; 3 | writeFile?(path: string, fileUpdatedContent: string): Promise; 4 | copyFile?(source: string, destination: string): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/manifest.interface.ts: -------------------------------------------------------------------------------- 1 | import { BundleItem } from './bundle.interface'; 2 | 3 | export interface Manifest { 4 | conatinerId?: string; 5 | name: string; 6 | type: 'app' | 'service' | 'webcomponent' | 'web component'; 7 | bundle?: BundleItem[]; 8 | globalBundle?: BundleItem[]; 9 | version: string; 10 | dependencies?: { [key: string]: string }; 11 | } 12 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import { join } from 'path'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | app.enableCors(); 8 | app.useStaticAssets(join(__dirname, 'micro-app-registry')); 9 | await app.listen(3000); 10 | } 11 | bootstrap(); 12 | -------------------------------------------------------------------------------- /src/modules/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ 6 | { 7 | provide: ConfigService, 8 | useValue: new ConfigService(`${process.env.NODE_ENV || 'development'}.env`), 9 | }, 10 | ], 11 | exports: [ConfigService], 12 | }) 13 | export class ConfigModule {} 14 | -------------------------------------------------------------------------------- /src/modules/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv'; 2 | import * as fs from 'fs'; 3 | 4 | export class ConfigService { 5 | private readonly envConfig: { [key: string]: string }; 6 | 7 | constructor(filePath: string) { 8 | this.envConfig = dotenv.parse(fs.readFileSync(filePath)); 9 | } 10 | 11 | get(key: string): string { 12 | return this.envConfig[key]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/registry/registry.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Header, Options, Param } from '@nestjs/common'; 2 | import { RegistryService } from './registry.service'; 3 | 4 | @Controller('registry') 5 | export class RegistryController { 6 | constructor(private readonly appService: RegistryService) {} 7 | 8 | @Options(':microAppName') 9 | @Header('Access-Control-Allow-Headers', '*') 10 | @Header('Access-Control-Allow-Origin', 'http://localhost:9000') 11 | @Header('Access-Control-Allow-Credentials', 'true') 12 | @Header('Access-Control-Allow-Methods', 'OPTIONS') 13 | getAppOptions(@Param('microAppName') microAppName: string): string { 14 | return 'true'; 15 | } 16 | 17 | @Get(':microAppName') 18 | @Header('Access-Control-Allow-Origin', 'http://localhost:9000') 19 | @Header('Access-Control-Allow-Credentials', 'true') 20 | @Header('Access-Control-Allow-Methods', 'GET') 21 | @Header('Cache-Control', 'none') 22 | getApp(@Param('microAppName') microAppName: string): Promise { 23 | return this.appService.getMicroApp(microAppName.replace('.js', '')); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/registry/registry.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RegistryController } from './registry.controller'; 3 | import { RegistryService } from './registry.service'; 4 | import { UtilityModule } from '../../untilities/utility.module'; 5 | import { ConfigModule } from '../config/config.module'; 6 | 7 | @Module({ 8 | imports: [UtilityModule, ConfigModule], 9 | controllers: [RegistryController], 10 | providers: [RegistryService], 11 | }) 12 | export class RegistryModule {} 13 | -------------------------------------------------------------------------------- /src/modules/registry/registry.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { join } from 'path'; 3 | import * as dashify from 'dashify'; 4 | import * as strip from 'strip-comments'; 5 | import * as uniqid from 'uniqid'; 6 | import * as cheerio from 'cheerio'; 7 | import { FileUtils } from '../../untilities/file.utils'; 8 | import { Manifest } from '../../interfaces/manifest.interface'; 9 | import { HTMLUtils } from '../../untilities/html.utils'; 10 | import { JSUtils } from '../../untilities/js.utils'; 11 | import { TemplateUtils } from '../../untilities/template.utils'; 12 | import { ConfigService } from '../config/config.service'; 13 | import { RemoteFileUtils } from '../../untilities/remote-file.utils'; 14 | import { FileAccessor } from '../../interfaces/file-accessor.interface'; 15 | 16 | @Injectable() 17 | export class RegistryService { 18 | fileAccessor: FileAccessor = this.fileUtils; 19 | constructor( 20 | private readonly fileUtils: FileUtils, 21 | private readonly remoteFileUtils: RemoteFileUtils, 22 | private readonly htmlUtils: HTMLUtils, 23 | private readonly jsUtils: JSUtils, 24 | private readonly templateUtils: TemplateUtils, 25 | private readonly config: ConfigService 26 | ) {} 27 | 28 | async getMicroApp(microAppName: string): Promise { 29 | const registryPath = `${__dirname}/../../${this.config.get('REGISTRY_PATH')}`; 30 | const externals = await this.fileUtils.readFile(`${registryPath}/external.json`).then(file => JSON.parse(file)); 31 | const isRemote: boolean = Object.keys(externals).indexOf(microAppName) > -1; 32 | this.fileAccessor = isRemote ? this.remoteFileUtils : this.fileUtils; 33 | const containerId = uniqid('app-root-'); 34 | const appRootPath = isRemote 35 | ? externals[microAppName] 36 | : `${registryPath}/${microAppName}`; 37 | 38 | return this.fileAccessor 39 | .readFile(`${appRootPath}/micro-fe-manifest.json`) 40 | .then(manifestAsText => typeof manifestAsText === 'string' ? JSON.parse(manifestAsText) : manifestAsText) 41 | .then((manifest: Manifest) => { 42 | const { globalBundle = [], bundle = [], type = 'default', name } = manifest; 43 | const globalBundleFixedPaths = globalBundle.map(bundle => ({ 44 | ...bundle, 45 | path: `${this.config.get('DOMAIN')}/${name}/${bundle.path}`, 46 | })); 47 | let jsFilePaths = 48 | bundle.filter(({ type }) => type === 'js').map(({ path }) => `${appRootPath}/${path}`) || []; 49 | let inlineJSPieces = []; 50 | const styleLinks = bundle 51 | .filter(({ type }) => type === 'css') 52 | .map(({ path }) => `${this.config.get('DOMAIN')}/${name}/${path}`); 53 | 54 | const htmlTemplatePromises = bundle 55 | .filter(({ type }) => type === 'template' || type === 'html') 56 | .map(({ path }) => `${appRootPath}/${path}`) 57 | .map(path => 58 | this.fileAccessor 59 | .readFile(path) 60 | .then(file => cheerio.load(file)) 61 | .then($ => { 62 | jsFilePaths = [ 63 | ...this.htmlUtils.getLocalPathsToJsFiles($, appRootPath), 64 | ...jsFilePaths, 65 | ]; 66 | return $; 67 | }) 68 | .then($ => { 69 | inlineJSPieces = [...this.htmlUtils.getInlineJSPieces($), ...inlineJSPieces]; 70 | return $; 71 | }) 72 | .then($ => 73 | this.htmlUtils.fixRelativeInlineStylePaths($, `${this.config.get('DOMAIN')}/${name}`) 74 | ) 75 | .then($ => this.htmlUtils.moveStylesToBody($)) 76 | .then($ => this.htmlUtils.fixRelativeHtmlPaths($, name)) 77 | .then($ => this.htmlUtils.cleanScriptTags($)) 78 | .then($ => this.htmlUtils.extractBodyArea($)) 79 | ); 80 | 81 | return Promise.all(htmlTemplatePromises) 82 | .then(htmlTemplates => htmlTemplates.join('')) // concat html templates 83 | .then(htmlTemplate => 84 | Promise.all( 85 | jsFilePaths.map(path => 86 | this.fileAccessor 87 | .readFile(path) 88 | .then(f => `/* ${path.split('/')[path.split('/').length - 1]} */ ${strip(f, {})}`) 89 | ) 90 | ) 91 | .then(files => [...inlineJSPieces, ...files].join(' ')) // concat js files 92 | .then(file => this.jsUtils.fixRelativePathsInJs(name, file)) 93 | .then(file => this.jsUtils.fixDocumentAccessJs(file)) 94 | .then(appContentAsText => 95 | this.wrapTheApp({ 96 | appContentAsText, 97 | ...manifest, 98 | containerId, 99 | htmlTemplate: this.htmlUtils.composeTemplate(styleLinks, htmlTemplate, containerId), 100 | type, 101 | globalBundle: globalBundleFixedPaths, 102 | }) 103 | ) 104 | ); 105 | }); 106 | } 107 | 108 | wrapTheApp({ 109 | containerId, 110 | appContentAsText = '', 111 | name = '', 112 | dependencies = {}, 113 | htmlTemplate = '', 114 | type, 115 | globalBundle = [], 116 | }): Promise { 117 | const parsedDep = Object.keys(dependencies) 118 | .map(dep => '\'' + dep + '\'') 119 | .join(', '); 120 | const globalInjectListAsString = JSON.stringify(globalBundle); 121 | const encapsulatedWebPackAppContentAsText = appContentAsText.replace(/webpackJsonp/g, `webpackJsonp__${name}`); 122 | return (() => 123 | globalBundle.length 124 | ? this.fileUtils 125 | .readFile(this.templateUtils.templatePath('global')) 126 | .then(globalInjectTemplate => 127 | globalInjectTemplate.replace(/__global-inject-list__/g, globalInjectListAsString) 128 | ) 129 | : Promise.resolve(''))().then(parsedGlobalInject => 130 | this.fileUtils.readFile(this.templateUtils.templatePath(type)).then(template => 131 | template 132 | .replace(/__kebab-name__/g, dashify(name)) 133 | .replace(/__container_id__/g, containerId) 134 | .replace(/__name__/g, name) 135 | .replace(/__htmlTemplate__/g, htmlTemplate) 136 | .replace(/__dependencies__/g, parsedDep) 137 | .replace(/__appContentAsText__/g, encapsulatedWebPackAppContentAsText) 138 | .replace(/__global-inject__/g, parsedGlobalInject) 139 | ) 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/tasks/css.tasks.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cron, NestSchedule, defaults } from 'nest-schedule'; 3 | import { join } from 'path'; 4 | import * as walk from 'walk'; 5 | import * as fs from 'fs'; 6 | import { CSSUtils } from '../untilities/css.utils'; 7 | import { FileUtils } from '../untilities/file.utils'; 8 | 9 | defaults.enable = true; 10 | defaults.maxRetry = -1; 11 | defaults.retryInterval = 5000; 12 | 13 | @Injectable() 14 | export class CssTasks extends NestSchedule { 15 | constructor(private readonly fileUtils: FileUtils, private readonly cssUtils: CSSUtils) { 16 | super(); 17 | } 18 | 19 | @Cron('*/10 * * * *') 20 | async fixRelativeCssPathsInAllApps() { 21 | // every ten minutes check all unhandled files and fix paths 22 | const files = await this.getAppRootPathsList(join(__dirname, '../', 'micro-app-registry')); 23 | files.forEach(file => { 24 | const fileFolder = file.path.split('src/micro-app-registry')[1].replace(`${file.name}`, ''); 25 | const path = `http://localhost:3000${fileFolder}`; 26 | this.fileUtils 27 | .readFile(file.path) 28 | .then(fileContent => { 29 | this.fileUtils.copyFile(file.path, `${file.path}.original`); 30 | return fileContent; 31 | }) 32 | .then(fileContent => this.cssUtils.fixRelativePathsInCss(path, fileContent)) 33 | .then(fileUpdatedContent => this.fileUtils.writeFile(file.path, fileUpdatedContent)); 34 | }); 35 | console.log('**************************************************'); 36 | console.log(`CRON TASK: fixRelativeCssPathsInAllApps (${new Date().toString()})`); 37 | console.log('--------------------------------------------------'); 38 | if (files.length > 0) { 39 | console.log(`\n(${files.length}) PROCESSED FILES\n`); 40 | console.log(files.map(file => file.path).join('\n')); 41 | } else { 42 | console.log(`\nNO FILES FOUND TO PROCESS\n`); 43 | } 44 | console.log('**************************************************'); 45 | } 46 | 47 | private async getAppRootPathsList(path: string): Promise<{ name: string; path: string; root: string }[]> { 48 | return await new Promise(resolve => { 49 | const files = []; 50 | const walker = walk.walk(path, { followLinks: false }); 51 | walker.on('file', function(root, stat, next) { 52 | const path = join(root, stat.name); 53 | if (stat.name.match('.css') && !stat.name.match('.original') && !fs.existsSync(`${path}.original`)) { 54 | files.push({ name: stat.name, path, root }); 55 | } 56 | next(); 57 | }); 58 | 59 | walker.on('end', function() { 60 | resolve(files); 61 | }); 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tasks/task.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CssTasks } from './css.tasks'; 3 | import { UtilityModule } from '../untilities/utility.module'; 4 | 5 | @Module({ 6 | imports: [UtilityModule], 7 | providers: [CssTasks] 8 | }) 9 | export class TaskModule { 10 | constructor(private cssTask: CssTasks) { 11 | cssTask.fixRelativeCssPathsInAllApps(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/templates/app.wrapper.template.js: -------------------------------------------------------------------------------- 1 | (function(window, document) { 2 | if (window && window.AppsManager && window.AppsManager.register) { 3 | window.AppsManager.register({ 4 | name: '__name__', 5 | deps: [__dependencies__], 6 | initialize: microAppArgs => { 7 | __global-inject__ 8 | class __name__ extends HTMLElement { 9 | constructor() { 10 | super(); 11 | this.attachShadow({ mode: 'open' }); 12 | const template = document.createElement('template'); 13 | template.id = 'template___container_id__'; 14 | template.innerHTML = `__htmlTemplate__`; 15 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 16 | } 17 | 18 | connectedCallback() { 19 | if (window.webpackJsonp____name__ && window.webpackJsonp____name__.length) { 20 | delete window.webpackJsonp____name__; 21 | } 22 | const MICROAPP_CONTAINER = this.shadowRoot.getElementById('__container_id__'); 23 | const DOCUMENT = this.shadowRoot; 24 | microAppArgs['container'] = MICROAPP_CONTAINER; 25 | microAppArgs['microAppId'] = '__container_id__'; 26 | __appContentAsText__; 27 | } 28 | } 29 | customElements.define('__kebab-name__', __name__); 30 | }, 31 | }); 32 | } 33 | })(window, document); 34 | -------------------------------------------------------------------------------- /src/templates/global-inject.template.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const globalBundle = __global-inject-list__; 3 | const $head = document.getElementsByTagName('head')[0]; 4 | globalBundle.forEach(bundle => { 5 | switch(bundle.type) { 6 | case 'js': 7 | const script = document.createElement('script'); 8 | script.src = bundle.path; 9 | $head.appendChild(script); 10 | break; 11 | case 'css': 12 | const link = document.createElement('link'); 13 | link.href = bundle.path; 14 | link.rel = 'stylesheet'; 15 | $head.appendChild(link); 16 | break; 17 | } 18 | }) 19 | })(); 20 | -------------------------------------------------------------------------------- /src/templates/service.wrapper.template.js: -------------------------------------------------------------------------------- 1 | (function(WINDOW, DOCUMENT) { 2 | window = undefined; 3 | if (WINDOW && WINDOW.AppsManager && WINDOW.AppsManager.register) { 4 | WINDOW.AppsManager.register({ 5 | name: '__name__', 6 | deps: [__dependencies__], 7 | initialize: (microAppArgs) => { 8 | __global-inject__ 9 | return __appContentAsText__ 10 | } 11 | }); 12 | } 13 | })(window, document); 14 | -------------------------------------------------------------------------------- /src/templates/web-component.wrapper.template.js: -------------------------------------------------------------------------------- 1 | (function(WINDOW, DOCUMENT) { 2 | window = undefined; 3 | if (WINDOW && WINDOW.AppsManager && WINDOW.AppsManager.register) { 4 | WINDOW.AppsManager.register({ 5 | name: '__name__', 6 | deps: [__dependencies__], 7 | initialize: (microAppArgs) => { 8 | __global-inject__ 9 | class __name__ extends HTMLElement { 10 | constructor() { 11 | super(); 12 | this.attachShadow({ mode: 'open' }); 13 | const template = document.createElement('template'); 14 | template.id = 'template___container_id__'; 15 | template.innerHTML = `__htmlTemplate__`; 16 | this.shadowRoot.appendChild(template.content.cloneNode(true)); 17 | } 18 | 19 | connectedCallback() { 20 | if (window.webpackJsonp____name__ && window.webpackJsonp____name__.length) { 21 | delete window.webpackJsonp____name__; 22 | } 23 | } 24 | } 25 | customElements.define('__kebab-name__', __name__); 26 | __appContentAsText__; 27 | }, 28 | }); 29 | } 30 | })(window, document); 31 | -------------------------------------------------------------------------------- /src/untilities/css.utils.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class CSSUtils { 5 | fixRelativePathsInCss(path: string, file: string) { 6 | const relativePathPatternInQuoute = /(?<=url\((["']))((?!data:image)(?!http).)*?(?=\1\))/g; 7 | const relativePathPatternNoQuoute = /(?<=url\()((?!["'])(?!data:image)(?!http).)*?(?=\))/g; 8 | return file 9 | .replace(relativePathPatternInQuoute, `${path}$&`) 10 | .replace(relativePathPatternNoQuoute, `${path}$&`) 11 | .replace(/html|body/g, `:host`); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/untilities/file.utils.ts: -------------------------------------------------------------------------------- 1 | import { FileAccessor } from '../interfaces/file-accessor.interface'; 2 | import { Injectable } from '@nestjs/common'; 3 | import * as fs from 'fs'; 4 | 5 | @Injectable() 6 | export class FileUtils implements FileAccessor { 7 | copyFile(source: string, destination: string): Promise { 8 | return new Promise((resolve, reject) => 9 | fs.copyFile(source, destination, (err: Error) => (err ? reject(err) : resolve())) 10 | ); 11 | } 12 | 13 | readFile(path: string): Promise { 14 | return new Promise((resolve, reject) => 15 | fs.readFile(path, { encoding: 'UTF8' }, (err: Error, file: string) => 16 | err ? reject(err) : resolve(file) 17 | ) 18 | ); 19 | } 20 | 21 | writeFile(path: string, fileUpdatedContent: string): Promise { 22 | return new Promise((resolve, reject) => 23 | fs.writeFile(path, Buffer.from(fileUpdatedContent, 'utf8'), (err: Error) => 24 | err ? reject(err) : resolve() 25 | ) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/untilities/html.utils.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { CSSUtils } from './css.utils'; 3 | import { Injectable } from '@nestjs/common'; 4 | 5 | @Injectable() 6 | export class HTMLUtils { 7 | constructor(private readonly cssUtils: CSSUtils) {} 8 | 9 | composeTemplate(styleLinks: any[], htmlTemplate: string, containerId: string) { 10 | return [ 11 | ...styleLinks.map(link => ``), 12 | htmlTemplate, 13 | !htmlTemplate || htmlTemplate === '' ? `` : undefined, 14 | ].join(''); 15 | } 16 | 17 | moveStylesToBody($) { 18 | const $body = $('body'); 19 | const $links = $('head link[rel="stylesheet"]'); 20 | const $styles = $('head style'); 21 | if ($body.length > 0) { 22 | $styles.each((index, $style) => { 23 | $body.prepend($style); 24 | }); 25 | $links.each((index, $link) => { 26 | $body.prepend($link); 27 | }); 28 | } 29 | return $; 30 | } 31 | 32 | extractBodyArea($): string { 33 | const $body = $('body'); 34 | return $body.length > 0 ? $body.html() : $.html(); 35 | } 36 | 37 | cleanScriptTags($) { 38 | $('script').remove(); 39 | return $; 40 | } 41 | 42 | fixRelativeHtmlPaths($, name: string) { 43 | $('img').each(function() { 44 | let uri = $(this).attr('src'); 45 | if (uri && uri !== '' && uri.search('http') === -1) { 46 | uri = `http://localhost:3000/${join(name, uri)}`; 47 | $(this).attr('src', uri); 48 | } 49 | }); 50 | $('link').each(function() { 51 | let uri = $(this).attr('href'); 52 | if (uri && uri !== '' && uri.search('http') === -1) { 53 | uri = `http://localhost:3000/${join(name, uri)}`; 54 | $(this).attr('href', uri); 55 | } 56 | }); 57 | return $; 58 | } 59 | 60 | fixRelativeInlineStylePaths($, path: string) { 61 | $('style').each(function() { 62 | let style = $(this).html(); 63 | $(this).html(this.cssUtils.fixRelativePathsInCss(style, path)); 64 | }); 65 | return $; 66 | } 67 | 68 | getLocalPathsToJsFiles($, appRootPath: string): string[] { 69 | const paths = []; 70 | $('script').each(function() { 71 | const path = $(this).attr('src'); 72 | if (path && path.search('http') === -1) { 73 | paths.push(`${appRootPath}/${path}`); 74 | } 75 | }); 76 | return paths; 77 | } 78 | 79 | getInlineJSPieces($): string[] { 80 | const inlinePieces = []; 81 | $('script').each(function() { 82 | const text = $(this).html(); 83 | if (text && text !== '') { 84 | inlinePieces.push(text + ';'); 85 | } 86 | }); 87 | return inlinePieces; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/untilities/js.utils.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class JSUtils { 5 | fixRelativePathsInJs(name, file) { 6 | const path = `http://localhost:3000/${name}/`; 7 | return file.replace(/((?<=(["'])(?!http))[.\/a-zA-Z0-9\-_]*?)(\.((sv|pn)g)|(jpe?g)|(gif))(?=\2)/g, `${path}$&`); 8 | } 9 | 10 | fixDocumentAccessJs(file) { 11 | return file.replace(/document.get/g, 'DOCUMENT.get'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/untilities/remote-file.utils.ts: -------------------------------------------------------------------------------- 1 | import { FileAccessor } from '../interfaces/file-accessor.interface'; 2 | import { HttpService, Injectable } from '@nestjs/common'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export class RemoteFileUtils implements FileAccessor { 7 | constructor(private readonly http: HttpService) {} 8 | readFile(path: string): Promise { 9 | console.log(path); 10 | return this.http.get(path, {responseType: 'text'}) 11 | .pipe(map(response => response.data)) 12 | .toPromise(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/untilities/template.utils.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class TemplateUtils { 5 | base = `${__dirname}/../templates/`; 6 | 7 | templatePath(type) { 8 | return `${this.base}${this.templateFileName(type)}`; 9 | } 10 | 11 | templateFileName(type) { 12 | switch (type) { 13 | case 'webcomponent': 14 | case 'web component': 15 | return 'web-component.wrapper.template.js'; 16 | case 'service': 17 | return 'service.wrapper.template.js'; 18 | case 'global': 19 | return 'global-inject.template.js'; 20 | default: 21 | return 'app.wrapper.template.js'; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/untilities/utility.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule, Module } from '@nestjs/common'; 2 | import { FileUtils } from './file.utils'; 3 | import { TemplateUtils } from './template.utils'; 4 | import { CSSUtils } from './css.utils'; 5 | import { HTMLUtils } from './html.utils'; 6 | import { JSUtils } from './js.utils'; 7 | import { RemoteFileUtils } from './remote-file.utils'; 8 | 9 | @Module({ 10 | imports: [HttpModule], 11 | providers: [ 12 | { provide: FileUtils, useValue: new FileUtils() }, 13 | RemoteFileUtils, 14 | TemplateUtils, 15 | CSSUtils, 16 | HTMLUtils, 17 | JSUtils, 18 | ], 19 | exports: [ 20 | FileUtils, 21 | RemoteFileUtils, 22 | TemplateUtils, 23 | CSSUtils, 24 | HTMLUtils, 25 | JSUtils, 26 | ] 27 | }) 28 | export class UtilityModule {} 29 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "outDir": "./dist", 14 | "baseUrl": "./" 15 | }, 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["jest", "node"] 5 | }, 6 | "include": ["**/*.spec.ts", "**/*.d.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "jsRules": { 4 | "class-name": true, 5 | "comment-format": [ 6 | true, 7 | "check-space" 8 | ], 9 | "indent": [ 10 | true, 11 | "spaces" 12 | ], 13 | "no-duplicate-variable": true, 14 | "no-eval": true, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "one-line": [ 18 | true, 19 | "check-open-brace", 20 | "check-whitespace" 21 | ], 22 | "quotemark": [ 23 | true, 24 | "single" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "triple-equals": [ 31 | true, 32 | "allow-null-check" 33 | ], 34 | "variable-name": [ 35 | true, 36 | "ban-keywords" 37 | ], 38 | "whitespace": [ 39 | true, 40 | "check-branch", 41 | "check-decl", 42 | "check-operator", 43 | "check-separator", 44 | "check-type" 45 | ] 46 | }, 47 | "rules": { 48 | "class-name": true, 49 | "comment-format": [ 50 | true, 51 | "check-space" 52 | ], 53 | "indent": [ 54 | true, 55 | "spaces" 56 | ], 57 | "no-eval": true, 58 | "no-internal-module": true, 59 | "no-trailing-whitespace": true, 60 | "no-unsafe-finally": true, 61 | "no-var-keyword": false, 62 | "one-line": [ 63 | true, 64 | "check-open-brace", 65 | "check-whitespace" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": [ 72 | true, 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "variable-name": [ 90 | true, 91 | "ban-keywords" 92 | ], 93 | "whitespace": [ 94 | true, 95 | "check-branch", 96 | "check-decl", 97 | "check-operator", 98 | "check-separator", 99 | "check-type" 100 | ] 101 | } 102 | } 103 | --------------------------------------------------------------------------------