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