Repository: onerzafer/microfe-client Branch: master Commit: 577612460eed Files: 31 Total size: 38.2 KB Directory structure: gitextract_aqivtzgj/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .prettierrc ├── LICENSE.md ├── README.md ├── index.html ├── karma.config.js ├── package.json ├── spec.bundle.js ├── src/ │ ├── lib/ │ │ ├── AppsManager/ │ │ │ ├── AppsManager.ts │ │ │ ├── status.enum.ts │ │ │ └── tag.enum.ts │ │ ├── Bootstrapper/ │ │ │ └── bootstrapper.ts │ │ ├── Decorators/ │ │ │ └── Microfe.decorator.ts │ │ ├── Interfaces/ │ │ │ ├── AppsManager.interface.ts │ │ │ ├── AppsManager.internal.interface.ts │ │ │ ├── Config.interface.ts │ │ │ └── Router.interface.ts │ │ ├── Loader/ │ │ │ └── Loader.ts │ │ ├── Provider/ │ │ │ └── Provider.ts │ │ ├── Router/ │ │ │ ├── Link.ts │ │ │ ├── Router.ts │ │ │ └── RouterOutlet.ts │ │ ├── Store/ │ │ │ └── MicroAppStore.ts │ │ └── index.ts │ └── main.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── webpack.production.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .gitignore ================================================ node_modules/ dist/ .idea/ ================================================ FILE: .prettierrc ================================================ { "trailingComma": "es5", "tabWidth": 4, "semi": true, "singleQuote": true, "jsxSingleQuote": true, "bracketSpacing": true, "jsxBracketSameLine": true, "printWidth": 120 } ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2018 Öner Zafer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # *microfe* *(microfe - short for micro frontends)* A naive infrastructure/meta-framework implementation for micro frontends. This project intends to provide the necessary tooling to achieve independent apps loaded separately and run on different parts on a single web page in complete isolation. For detailed information on the topic can be found [micro-frontends.org](https://micro-frontends.org/) ## Motivation When developing microservices there are lots of tools and libraries to help developers to focus the effort on the things needs to be done instead of fighting against a monolithic monster. For now, "micro frontends" idea is still premature and it needs time to grow something easy to use. My intention is to contribute to this discussion and also provide necessary tooling and a sample architecture for developers who would like to give it a try. Providing an easy to use infrastructure for individuals and companies can be considered as an ultimate goal. ## Who will/may/can use *microfe*? Ideally *microfe* is not suitable for small teams and for them trying to use it would not be necessary. For this kind of teams refactoring their monolithic fe apps would be more productive instead of using *microfe* to divide a relatively big app into smaller pieces and trying to maintain each piece. If the project contains at least two independent teams which are responsible for the same monolithic app then *microfe* can be beneficial. Because *microfe* gives the opportunity of working on independent tech stack by each team. It can provide isolation and managed communication channels between micro-apps. ## On Micro Frontends While companies growing they usually move from one team to two or more and they start to divide the code base and on the backend side microservice architecture has lots of benefits to scale the company up. On the frontend side, the code base becomes a growing monolith even if it is written in a modular fashion. So scaling a front end team is not so easy and problems start to appear. Lack of communication between teams, conflicting merges, hard to change tech stacks, hard to update dependencies and the list goes on. Similar to microservices, the micro frontends provides the opportunity to isolate code bases and make the teams free to use any code standards and tech stack and focus on relatively small parts of the application. ## Goals * Isolated and Independent apps * A way to have a unified UI * Inter-app communication (i.e. authentication) * Easy to maintain apps * Not to break already available build environments for major frameworks (React, Angular, Vue) * Freedom of tech stack choice ## Requirements To run the microfe locally you need to clone and run [micro-fe-registry](https://github.com/onerzafer/micro-fe-registry). The documentation for micro-fe-registry can be found under its own repository. ## Usage Currently, there is no npm package provided and the usage is not recommended at this phase. Yet if you are willing to experiment by yourself clone both repositories. For micro-fe-registry part follow the instructions on its own repository. Then execute following commands ```bash npm install npm start ``` This command will open your browser on http://localhost:8080 and you will be able to see the page is running. If you see just blank page be sure your micro-fe-registry installation is up and running. And if it is running already please check if the requested micro-apps are available on the registry folder with requested names. If you have still problems of running please open an issue I will be happy to help you. ## Top level architecture The microfe library basically has 4 different main parts *AppsManager, Loader, Router* and *Store*. It also provides some helper functions and classes: *Bootstrapper, Microfe decorator* and *provider*. These all parts of the microfe library can function with a specific micro-apps wich implements microfe interface. ### Definition of a microfe app A microfe app should implement the following interface ```TypeScript interface MicroApp { name: string; deps?: string[]; initialize: (args: { AppsManager: AppsManager; Config: ConfigInterface; [key: string]: any; }) => any | void; } ``` **initialize** function may return the instance of the app ### Bootstrapper The responsibility of Bootstrapper is initializing the AppsManager and all other micro-apps provided inside the library. So it can be considered as the entry point of the microfe library. The signature for bootstrapper can be described like this: ```TypeScript const bootstrap = ( routes: Route[], config: ConfigInterface ) => (...microApps: MicroApp[]) => void ``` To boostrap the microfe meta-framework the following example can be used as a refrence: ```TypeScript import { Microfe, Bootstrap, Route, ConfigInterface } from './lib'; @Microfe({ deps: ['LayoutApp'], }) class Main { constructor() { console.log('Initialised'); } } const Routes: Route[] = [ { path: '/', redirectTo: '/angular' }, { path: '/angular', microApp: 'demoAngular', tagName: 'demo-angular' }, { path: '/react', microApp: 'reactDemo', tagName: 'react-demo' }, { path: '/static', microApp: 'staticApp', tagName: 'static-app' }, { path: '*', microApp: 'NotFoundApp', tagName: 'not-found-app' }, ]; const Config: ConfigInterface = { registryApi: 'http://localhost:3000/registry', registryPublic: 'http://localhost:3000', }; Bootstrap(Routes, Config)(Main); ``` ### AppsManager The main functionality of AppsManager is creating the dependency tree and when all of the dependencies of a micro-app are ready, it instantiates the micro-app by providing the dependencies instances. The public API for AppsManager can be summarized as follows: ```TypeScript interface AppsManager { register: (app: MicroApp) => void; subscribe: (fn: (notFoundApps: MicroApp[]) => void) => { unsubscribe: () => void } } ``` AppsManager passes the config and itself as default dependency to all of the instances of provided micro-apps. Which means all have the access to AppsManager and its public API. Alternatively, AppsManager can be accessed from window global as AppsManager. *AppsManager is the only part which does not implement the MicroApp interface. The rest of the library actually is a collection of micro-apps.* ### Loader When registered by Bootstrapper the Loader requires Config and waits until it is provided. With the config Loader receives the micro-fe-registry public URLs. After getting the Config AppsManagers instantiates the Loader. On constructer Loader subscribes to AppsManagers and start the not found micro-apps. When a new not found micro-app available the Loader parse the micro-app URL by combining the name of the micro-app and public URL of micro-fe-registry injects it to the dom as a remote script. Naturally, the browser loads the micro-app from given URL. The loader can be a dependency and it has only one public function. ```TypeScript const Loader { fetchMicroApp: (name: string) => void } ``` ### Router Unlike the common routers, the microfe router has limited functionality. It is capable of solving the first part of the declared URLs. This implementation assumes the rest of the URL will be resolved by the responsible micro-app. If the Router can resolve the URL from the browser location it triggers the Loader.fetchMicroApp function with the name of resolved micro-app. So it has two dependencies routes and Loader. #### Route The routes object is an array of the Route objects which has the following interface: ```TypeScript interface Route { path: string; tagName?: string; redirectTo?: string; microApp?: string; } ``` #### micro-router the Router outlet When Router instantiates it register a web component called micro-router. This is the expected place for all other micro-apps loads on route hit. The usage is pretty simple and available for all micro-apps living on the client at the moment. ```html ``` Currently, it has no targetting of sub-routes. Which means all of the micro-router tags will display the same target micro-app. So current recommendations are using only one micro-router the page. In the future, some sub-routes can be targetted to some named micro-router tags. #### micro-link Router also provides a simple navigation element with no design. All micro-apps will be able to access it any time since it is provided as a web component like micro-router. micro-link has one attribute which is href and if the given path is the current route, it assigns itself automatically active class. So no need to observe history and match correct path to put active class to the links. ```html Some Cool Page ``` When we navigate to /some/cool/page this micro-link above will be marked as active. ### Store With the assumption of only big teams and big code bases will need the microfe and nearly all of the already managing app state, the microfe library provides a global shared inter-app state. this state can be used as a shared event bus or shared global state. By nature, this store is reactive and powered by RxJS. Yet it still has the similar functionalities of Redux library. ```TypeScript interface Action { type: string; [key: string]: any; } interface State { [key: string]: any; } interface Reducer { (action: Action, state: State) => State) => void; } interface ReducerTreePiece { [key: string]: Reducer | ReducerTreePiece } interface MicroAppStore { addReducer: (reducerTreePiece: ReducerTreePiece) => void; dispatch: (action: Action) => void; select: (selector: string) => Observable; } ``` The main issue with the MicroAppStore is the reducers may arrive on different times. The select function is pretty useful on this case. Because if the selected reducer is not available it sibly emits undefined and when the reducer arrives it emmits to all subscribers the related state. ```TypeScript const Todos$ = MicroAppsStore.select('todos'); Todos$.subscribe(todos => console.log(todos)); // immediatelly logs undefined const todosReducer = (state = [], action: Action) => { return state; } MicroAppsStore.addReducer({todos: todoRecucer}); // At this point Todos$.subscribe will receive [] as todos and will log [] ``` ### Microfe decorator This decorator can be used as an helper for casting any class to a micro-app ```TypeScript @Microfe({ deps: ['LayoutApp'] }) export class Main { private layoutApp; constructor({LayoutApp}) { this.layoutApp = LayoutApp; this.render(); } private render() { this.layoutApp.someLayoutAppFunction(); } } ``` The code block above will be equavalent to following code: ```javascript { name: 'Main', deps: ['LayoutApp'] initialize: function({LayoutApp}) { LayoutApp.someLayoutAppFunction(); } } ``` ### Provider The provider is a helper function to provide objects as micro-app. So any static data can be provided to other micro-apps with Provide function: ```TypeScript const languageEn = {hello: 'Hello'}; const languageEnProvider = provide(languageEn); ``` Then languageEnProvider can be passed down to all micro-apps which has the dependency as follows: ```TypeScript Bootstrap(Routes, Config)(Main, LanguageEnProvider); ``` # License [MIT](https://choosealicense.com/licenses/mit/) ================================================ FILE: index.html ================================================ Test App ================================================ FILE: karma.config.js ================================================ const webpackConfig = require('./webpack.config.js'); webpackConfig.mode = 'production'; module.exports = function(config) { config.set({ singleRun: true, browsers: [ 'PhantomJS' ], frameworks: [ 'jasmine' ], files: [ 'spec.bundle.js' ], preprocessors: { 'spec.bundle.js': ['webpack'] }, webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, plugins: [ require('karma-jasmine'), require('karma-phantomjs-launcher'), require('karma-webpack') ] }); }; ================================================ FILE: package.json ================================================ { "name": "micro-fe", "version": "0.0.1", "description": "A naive micro frontend solution", "main": "index.js", "scripts": { "start": "webpack-dev-server --progress --open --history-api-fallback", "build": "webpack --config webpack.production.config.js --mode production", "test": "karma start karma.config.js" }, "author": "Öner Zafer", "license": "MIT", "devDependencies": { "@types/jasmine": "2.8.7", "@types/node": "7.0.0", "jasmine-core": "3.1.0", "karma": "2.0.4", "karma-jasmine": "1.1.2", "karma-phantomjs-launcher": "1.0.4", "karma-webpack": "3.0.0", "ts-loader": "^4.1.0", "typescript": "^2.6.2", "tslint": "5.10.0", "tslint-loader": "3.6.0", "webpack": "^4.28.4", "webpack-cli": "^3.2.1", "webpack-dev-server": "^3.1.14", "fork-ts-checker-webpack-plugin": "^0.5.2" }, "dependencies": { "rxjs": "^6.3.3" } } ================================================ FILE: spec.bundle.js ================================================ var testsContext = require.context(".", true, /.spec.ts/); testsContext.keys().forEach(testsContext); ================================================ FILE: src/lib/AppsManager/AppsManager.ts ================================================ import { STATUS } from './status.enum'; import { MicroAppProvider } from '../Interfaces/AppsManager.interface'; import { AnyObj, BoolObj, MicroAppDef, MicroAppsGraph } from '../Interfaces/AppsManager.internal.interface'; export class AppsManager { private microAppsGraph: MicroAppsGraph = {}; private instanceCache: AnyObj = {}; private subscriptions: Array<(appList: MicroAppDef[]) => void> = []; constructor() { window['AppsManager'] = this; } register(microApp: MicroAppProvider | any) { const microAppDef = AppsManager.generateMicroAppDef(microApp); const tempGraph = { ...this.microAppsGraph, [microAppDef.name]: microAppDef }; if (this.isDefinedBefore(microAppDef.name)) { console.error(`[Conflict error]: "${microAppDef.name}" is defined before.`); return; } if (this.isCyclic(microAppDef.name, undefined, undefined, tempGraph)) { const deps = microApp.deps.join(', '); console.error(`[Dependency error]: "${microApp.name}" has cyclic dependency. Check "${deps}"`); return; } this.addMicroAppToGraph(microAppDef); microAppDef.deps.forEach(microAppName => { const dep = this.microAppsGraph[microAppName]; if (!dep) { this.addMicroAppToGraph(AppsManager.generatePlaceholderMicroAppDef(microAppName)); } }); this.updateMicroAppStatuses(); this.runReadyMicroApps(); this.dispatch(); } isDefinedBefore(microAppName: string): boolean { return this.microAppsGraph[microAppName] && this.microAppsGraph[microAppName].status !== STATUS.NOTFOUND; } subscribe(fn: (appList: MicroAppDef[]) => void) { this.subscriptions.push(fn); } dispatch() { const appList = Object.keys(this.microAppsGraph).map(microAppName => this.microAppsGraph[microAppName]); const notFoundList = appList.filter(microApp => microApp.status === STATUS.NOTFOUND); this.subscriptions.forEach(fn => { fn.call(null, notFoundList); }); } private checkDepsRunning(microApp: MicroAppDef): boolean { return ( !microApp.deps.length || (microApp.deps.length && microApp.deps.filter(microAppName => { return this.microAppsGraph[microAppName].status === STATUS.RUNNING; }).length === microApp.deps.length) ); } private isCyclic(vertex: string, visited: BoolObj = {}, recStack: BoolObj = {}, list: MicroAppsGraph): boolean { if (!visited[vertex]) { visited[vertex] = true; recStack[vertex] = true; const neighbours = (list[vertex] && list[vertex].deps) || []; for (let i = 0; i < neighbours.length; i++) { const current = neighbours[i]; if (!visited[current] && this.isCyclic(current, visited, recStack, list)) { return true; } else if (recStack[current]) { return true; } } } recStack[vertex] = false; return false; } private addMicroAppToGraph(microApp: MicroAppDef) { if (!this.microAppsGraph[microApp.name]) { this.microAppsGraph = { ...this.microAppsGraph, [microApp.name]: microApp }; } else { this.microAppsGraph = { ...this.microAppsGraph, [microApp.name]: { ...this.microAppsGraph[microApp.name], ...microApp, }, }; } } private runReadyMicroApps() { let hasSomethingRun = false; Object.keys(this.microAppsGraph) .filter( microAppName => this.microAppsGraph[microAppName].status === STATUS.READY && this.microAppsGraph[microAppName].app ) .forEach(microAppName => { const deps = this.provideDepsInstances(microAppName, this.microAppsGraph[microAppName].deps); this.instanceCache[microAppName] = this.microAppsGraph[microAppName].app.initialize(deps); this.microAppsGraph[microAppName].status = STATUS.RUNNING; hasSomethingRun = true; }); if (hasSomethingRun) { this.updateMicroAppStatuses(); } } private updateMicroAppStatuses() { let hasUpdated = false; Object.keys(this.microAppsGraph) .filter(microAppName => this.microAppsGraph[microAppName].status === STATUS.WAITING) .forEach(microAppName => { if (this.checkDepsRunning(this.microAppsGraph[microAppName])) { this.microAppsGraph[microAppName].status = STATUS.READY; hasUpdated = true; } }); if (hasUpdated) { this.runReadyMicroApps(); } } private provideDepsInstances(microAppName: string, deps: string[]): { [key: string]: any } { return deps.reduce( (cumulative, current) => { return { ...cumulative, [current]: this.instanceCache[current], }; }, { AppsManager: this, PATH: this.generateAppScopedPath(microAppName) } ); } private generateAppScopedPath(microAppName: string): string { return this.instanceCache.Config && this.instanceCache.Config.registryPublic ? `${this.instanceCache.Config.registryPublic}/${microAppName}` : ''; } static generateMicroAppDef(microApp: MicroAppProvider): MicroAppDef { return { name: microApp.name, status: microApp.deps ? STATUS.WAITING : STATUS.READY, deps: microApp.deps ? [...microApp.deps] : [], app: microApp, }; } static generatePlaceholderMicroAppDef(name: string): MicroAppDef { return { name, status: STATUS.NOTFOUND, deps: [], app: undefined, }; } } ================================================ FILE: src/lib/AppsManager/status.enum.ts ================================================ export enum STATUS { NOTFOUND = 0, WAITING = 1, READY = 2, RUNNING = 3, } ================================================ FILE: src/lib/AppsManager/tag.enum.ts ================================================ export enum TAG { script = 'script', style = 'style', } export enum TAG_TYPE { script = 'text/javascript', style = 'text/css', } ================================================ FILE: src/lib/Bootstrapper/bootstrapper.ts ================================================ import { AppsManager } from '../AppsManager/AppsManager'; import { Route } from '..'; import { Loader } from '../Loader/Loader'; import { MicroAppStore } from '../Store/MicroAppStore'; import { RouterOutlet } from '../Router/RouterOutlet'; import { MicroAppRouter } from '../Router/Router'; import { Provide } from '../Provider/Provider'; import { MicroLink } from '../Router/Link'; export const Bootstrap = ( routes?: Route[], config?: { registryApi: string; registryPublic: string; [key: string]: any } ) => (...entryApps: any[]) => { if (!entryApps.length) { throw new Error('At least one entry app should be provided'); } const manager = new AppsManager(); manager.register(Provide('Routes', routes || [])); manager.register(Provide('Config', config)); manager.register(MicroAppStore); manager.register(Loader); manager.register(MicroAppRouter); manager.register(RouterOutlet); manager.register(MicroLink); entryApps.forEach(entryApp => { manager.register(entryApp); }); }; ================================================ FILE: src/lib/Decorators/Microfe.decorator.ts ================================================ import { MicroAppMeta } from '..'; export const Microfe = function(meta?: MicroAppMeta ) { return function(WrappedClass) { WrappedClass.deps = meta && meta.deps; WrappedClass.initialize = (args) => new WrappedClass(args); }; }; ================================================ FILE: src/lib/Interfaces/AppsManager.interface.ts ================================================ export interface MicroAppMeta { deps?: string[]; } export interface MicroAppProvider extends MicroAppMeta { name: string; initialize: (deps: {[key: string]: any}) => void; } ================================================ FILE: src/lib/Interfaces/AppsManager.internal.interface.ts ================================================ import { STATUS } from '../AppsManager/status.enum'; import { MicroAppProvider } from './AppsManager.interface'; export interface MicroAppDef { name: string; // must be unique status: STATUS; deps: string[]; app: MicroAppProvider; } export interface MicroAppsGraph { [key: string]: MicroAppDef; } export interface BoolObj { [key: string]: boolean; } export interface AnyObj { [key: string]: any; } ================================================ FILE: src/lib/Interfaces/Config.interface.ts ================================================ export interface ConfigInterface { registryApi: string; registryPublic: string; [key: string]: any; } ================================================ FILE: src/lib/Interfaces/Router.interface.ts ================================================ export interface Route { path: string; tagName?: string; redirectTo?: string; microApp?: string; } export interface ResolvedRoute { path: string; resolvedPath: string; route: Route; query?: {[key: string]: string | number | boolean}; hash?: string; } ================================================ FILE: src/lib/Loader/Loader.ts ================================================ import { AppsManager } from '../AppsManager/AppsManager'; import { TAG, TAG_TYPE } from '../AppsManager/tag.enum'; import { Microfe } from '../Decorators/Microfe.decorator'; import { MicroAppDef } from '../Interfaces/AppsManager.internal.interface'; import { ConfigInterface } from '../Interfaces/Config.interface'; @Microfe({ deps: ['Config'] }) export class Loader { private loadingList: string[] = []; private readonly apiUrl: string = ''; private appsManager: AppsManager; constructor({AppsManager, Config}: {AppsManager: AppsManager, Config: ConfigInterface}) { this.apiUrl = (Config && Config.registryApi) || ''; this.appsManager = AppsManager; this.appsManager.subscribe(this.onNotFoundApp.bind(this)); } fetchMicroApp(microAppName: string) { if (!this.appsManager.isDefinedBefore(microAppName)) { const id = `${microAppName}_js_${new Date().getTime()}`; Loader.injectJsToHead(id, `${this.apiUrl}/${microAppName}.js`); } } private onNotFoundApp(appList: MicroAppDef[]) { appList.forEach(({ name }) => { if (this.loadingList.indexOf(name) === -1) { this.loadingList.push(name); this.fetchMicroApp(name); } }); } private static injectJsToHead(id: string, appUrl: string) { const script = document.createElement(TAG.script) as HTMLScriptElement; script.id = id; script.type = TAG_TYPE.script; script.src = appUrl; script.setAttribute('async', ''); const firstScriptTag = document.getElementsByTagName('script')[0]; firstScriptTag.parentNode.insertBefore(script, firstScriptTag); } } ================================================ FILE: src/lib/Provider/Provider.ts ================================================ export const Provide = (name: string, provideable: any) => ({ name, initialize: () => provideable, }); ================================================ FILE: src/lib/Router/Link.ts ================================================ import { MicroAppRouter } from './Router'; import { Microfe } from '../Decorators/Microfe.decorator'; @Microfe({ deps: ['MicroAppRouter'], }) export class MicroLink { constructor({ MicroAppRouter }: { MicroAppRouter: MicroAppRouter }) { class MicroLinkElement extends HTMLElement { private styleText = ` :host { display: inline-block; cursor: pointer; } `; constructor() { super(); this.attachShadow({ mode: 'open' }); MicroAppRouter.onChange(this.handleRouteChange); this.render(); } get href() { return this.getAttribute('href'); } set href(newValue) { this.setAttribute('href', newValue); } onclick = () => { MicroAppRouter.navigate(this.href); }; handleRouteChange = () => { MicroAppRouter.isActive(this.href) ? this.classList.add('active') : this.classList.remove('active'); }; private render = () => { const styleElm = document.createElement('style'); styleElm.innerHTML = this.styleText; this.shadowRoot.appendChild(styleElm); this.shadowRoot.appendChild(document.createElement('slot')); }; } customElements.define('micro-link', MicroLinkElement); } } ================================================ FILE: src/lib/Router/Router.ts ================================================ import { ResolvedRoute, Route } from '../Interfaces/Router.interface'; import { Microfe } from '../Decorators/Microfe.decorator'; @Microfe({ deps: ['Routes'], }) export class MicroAppRouter { private oldRoute: ResolvedRoute; private onChangeHandlers: Array<(oldPath: string, newPath: string) => void> = []; private routes: Array; constructor({ Routes }: { Routes: Array }) { this.routes = Routes; window.onpopstate = () => { this.navigate(window.location.pathname); }; this.navigate(window.location.pathname, true); } navigate(path: string, isSilent: boolean = false) { const resolvedRoute = this.resolve(MicroAppRouter.cleanPath(path)); if (resolvedRoute) { if (!this.oldRoute || this.oldRoute.path !== resolvedRoute.path) { if (!isSilent) { window.history.pushState(undefined, undefined, resolvedRoute.path); } this.changed(resolvedRoute); } } else { window.location.href = MicroAppRouter.cleanPath(path); } } onChange(fn: (oldPath: string, newPath: string, resolvedRoute?: ResolvedRoute) => void) { this.onChangeHandlers.push(fn); if (this.oldRoute) { fn.apply(null, [undefined, this.oldRoute.path, this.oldRoute]); } } isActive(pathToCheck: string): boolean { return this.oldRoute && MicroAppRouter.isHit(this.oldRoute.route, MicroAppRouter.cleanPath(pathToCheck)); } private changed(resolvedRoute: ResolvedRoute) { const oldPath = this.oldRoute && this.oldRoute.path; this.oldRoute = { ...resolvedRoute, }; this.onChangeHandlers.forEach(fn => { fn.apply(null, [oldPath, resolvedRoute.path, resolvedRoute]); }); } private resolve(path: string): ResolvedRoute { const foundRoute = this.routes.find(route => MicroAppRouter.isHit(route, path)); if (foundRoute && !foundRoute.redirectTo) { return { path: path, resolvedPath: foundRoute.path, route: { ...foundRoute, }, }; } else if (foundRoute && foundRoute.redirectTo) { return this.resolve(foundRoute.redirectTo); } else { return undefined; } } private static isHit(route: Route, path: string): boolean { return route.path === '/' ? route.path === path : MicroAppRouter.pathToRegexp(route.path).test(path); } private static pathToRegexp(path: string): RegExp { return new RegExp(`^${path.replace(/\\\//g, '/').replace('*', '.*?')}`); } private static cleanPath(path: string): string { return path && path.replace(new RegExp(window.location.origin), ''); } } ================================================ FILE: src/lib/Router/RouterOutlet.ts ================================================ import { Loader } from '../Loader/Loader'; import { MicroAppRouter } from './Router'; import { ResolvedRoute } from '../Interfaces/Router.interface'; import { Microfe } from '../Decorators/Microfe.decorator'; @Microfe({ deps: ['Loader', 'MicroAppRouter'], }) export class RouterOutlet { constructor({ Loader, MicroAppRouter }: { Loader: Loader; MicroAppRouter: MicroAppRouter }) { class RouterOutletElement extends HTMLElement { shadow = this.attachShadow({ mode: 'open' }); constructor() { super(); MicroAppRouter.onChange((oldPath, newPath, resolvedRoute) => this.handlePath(oldPath, newPath, resolvedRoute) ); } private handlePath(oldPath: string, newPath: string, resolvedRoute: ResolvedRoute) { if (resolvedRoute) { Loader.fetchMicroApp(resolvedRoute.route.microApp); const appTag = document.createElement(resolvedRoute.route.tagName); while (this.shadow.firstChild) { this.shadow.removeChild(this.shadow.firstChild); } this.shadow.appendChild(appTag); } } } customElements.define('micro-router', RouterOutletElement); } } ================================================ FILE: src/lib/Store/MicroAppStore.ts ================================================ import { BehaviorSubject } from 'rxjs'; import { pluck, scan } from 'rxjs/operators'; import { Microfe } from '../Decorators/Microfe.decorator'; @Microfe() export class MicroAppStore { private subject = new BehaviorSubject({}); private source = this.subject.pipe(scan((state, action) => this.applyReducers(state, action), this.subject.value)); private reducers = {}; constructor() { this.dispatch({type: '[@@MicroAppStore]: Init'}); } private applyReducers(state, action) { return Object.keys(this.reducers).reduce((cum, curr) => { return { ...cum, [curr]: this.reducers[curr](state[curr], action) || undefined, }; }, {}); } public addReducer(reducerTreePiece) { // TODO: validate the reducer schema this.reducers = { ...this.reducers, ...reducerTreePiece, }; } public dispatch(action: { type: string; payload?: any }) { this.subject.next(action); } public select(...selector) { return this.source.pipe(pluck(...selector)); } } ================================================ FILE: src/lib/index.ts ================================================ // PUBLIC INTERFACES export * from './Interfaces/AppsManager.interface'; export * from './Interfaces/Router.interface'; export * from './Interfaces/Config.interface'; // PUBLIC API export * from './Bootstrapper/bootstrapper'; export * from './Decorators/Microfe.decorator'; export * from './Provider/Provider'; ================================================ FILE: src/main.ts ================================================ import { Microfe, Bootstrap, Route, ConfigInterface } from './lib'; @Microfe({ deps: ['LayoutApp'], }) class Main { constructor() { console.log('Initialised'); } } const Routes: Route[] = [ { path: '/', redirectTo: '/angular' }, { path: '/angular', microApp: 'demoAngular', tagName: 'demo-angular' }, { path: '/react', microApp: 'reactDemo', tagName: 'react-demo' }, { path: '/static', microApp: 'staticApp', tagName: 'static-app' }, { path: '*', microApp: 'NotFoundApp', tagName: 'not-found-app' }, ]; const Config: ConfigInterface = { registryApi: 'http://localhost:3000/registry', registryPublic: 'http://localhost:3000', }; Bootstrap(Routes, Config)(Main); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "sourceMap": true, "typeRoots": ["node_modules/@types"], "lib": ["es2017", "dom"], "target": "es6", "moduleResolution": "node", "experimentalDecorators": true } } ================================================ FILE: tslint.json ================================================ { "jsRules": { "class-name": true, "comment-format": [ true, "check-space" ], "indent": [ true, "spaces" ], "no-duplicate-variable": true, "no-eval": true, "no-trailing-whitespace": true, "no-unsafe-finally": true, "one-line": [ true, "check-open-brace", "check-whitespace" ], "quotemark": [ true, "single" ], "semicolon": [ true, "always" ], "triple-equals": [ true, "allow-null-check" ], "variable-name": [ true, "ban-keywords" ], "whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type" ] }, "rules": { "class-name": true, "comment-format": [ true, "check-space" ], "indent": [ true, "spaces" ], "no-eval": true, "no-internal-module": true, "no-trailing-whitespace": true, "no-unsafe-finally": true, "no-var-keyword": false, "one-line": [ true, "check-open-brace", "check-whitespace" ], "quotemark": [ true, "single" ], "semicolon": [ true, "always" ], "triple-equals": [ true, "allow-null-check" ], "typedef-whitespace": [ true, { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" } ], "variable-name": [ true, "ban-keywords" ], "whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type" ] } } ================================================ FILE: webpack.config.js ================================================ 'use strict'; const rxPaths = require('rxjs/_esm5/path-mapping'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { devtool: 'inline-source-map', entry: './src/main.ts', output: { pathinfo: false, }, mode: 'development', optimization: { removeAvailableModules: false, removeEmptyChunks: false, splitChunks: false, }, module: { rules: [ { test: /\.tsx?$/, use: [ { loader: 'ts-loader', options: { transpileOnly: true, experimentalWatchApi: true, }, }, ], }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js'], modules: ['./node_modules'], alias: rxPaths(), }, plugins: [ new ForkTsCheckerWebpackPlugin({ tslintAutoFix: true, formatter: 'codeframe', }), ], }; ================================================ FILE: webpack.production.config.js ================================================ 'use strict'; const rxPaths = require('rxjs/_esm5/path-mapping'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); module.exports = { devtool: 'inline-source-map', entry: './src/main.ts', output: { pathinfo: false, }, mode: 'production', module: { rules: [ { test: /\.tsx?$/, use: [ { loader: 'ts-loader', options: { transpileOnly: false, }, }, ], }, ], }, resolve: { extensions: ['.ts', '.tsx', '.js'], modules: ['./node_modules'], alias: rxPaths(), }, plugins: [ new ForkTsCheckerWebpackPlugin({ tslintAutoFix: true, formatter: 'codeframe', }), ], };