Skip to content

Commit

Permalink
feat: ensure global services only be injected once #95
Browse files Browse the repository at this point in the history
  • Loading branch information
walkerkay committed May 18, 2020
1 parent eb749d5 commit 244f428
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 127 deletions.
27 changes: 22 additions & 5 deletions packages/planet/src/application/planet-application-loader.spec.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Subject } from 'rxjs';
import { RouterModule } from '@angular/router';
import { RouterModule, Router } from '@angular/router';
import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { PlanetApplicationLoader, ApplicationStatus } from './planet-application-loader';
import { AssetsLoader, AssetsLoadResult } from '../assets-loader';

import { SwitchModes, PlanetApplication } from '../planet.class';
import { PlanetApplicationService } from './planet-application.service';
import { NgZone } from '@angular/core';
import { NgZone, Injector, ApplicationRef } from '@angular/core';
import { PlanetApplicationRef } from './planet-application-ref';
import { app1, app2 } from '../test/applications';
import { Planet } from 'ngx-planet/planet';
import { getApplicationLoader, getApplicationService, clearGlobalPlanet, globalPlanet } from 'ngx-planet/global-planet';

class PlanetApplicationRefFaker {
planetAppRef: PlanetApplicationRef;
Expand Down Expand Up @@ -95,13 +97,15 @@ describe('PlanetApplicationLoader', () => {
let planetApplicationService: PlanetApplicationService;
let assetsLoader: AssetsLoader;
let ngZone: NgZone;
let planet: Planet;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterModule.forRoot([])]
});
planetApplicationLoader = TestBed.get(PlanetApplicationLoader);
planetApplicationService = TestBed.get(PlanetApplicationService);
planet = TestBed.get(Planet);
planetApplicationLoader = getApplicationLoader();
planetApplicationService = getApplicationService();
assetsLoader = TestBed.get(AssetsLoader);
ngZone = TestBed.get(NgZone);

Expand All @@ -115,7 +119,20 @@ describe('PlanetApplicationLoader', () => {
});

afterEach(() => {
(window as any).planet.apps = {};
clearGlobalPlanet();
});

it(`should repeat injection not allowed`, () => {
expect(() => {
return new PlanetApplicationLoader(
TestBed.get(AssetsLoader),
TestBed.get(PlanetApplicationService),
TestBed.get(NgZone),
TestBed.get(Router),
TestBed.get(Injector),
TestBed.get(ApplicationRef)
);
}).toThrowError('PlanetApplicationLoader has been injected in the portal, repeated injection is not allowed');
});

it(`should load (load assets and bootstrap) app1 success`, fakeAsync(() => {
Expand Down
11 changes: 9 additions & 2 deletions packages/planet/src/application/planet-application-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { AssetsLoader } from '../assets-loader';
import { PlanetApplication, PlanetRouterEvent, SwitchModes, PlanetOptions } from '../planet.class';
import { switchMap, share, map, tap, distinctUntilChanged, take, filter, catchError } from 'rxjs/operators';
import { getHTMLElement, coerceArray } from '../helpers';
import { PlanetApplicationRef, getPlanetApplicationRef, globalPlanet } from './planet-application-ref';
import { PlanetApplicationRef } from './planet-application-ref';
import { PlanetPortalApplication } from './portal-application';
import { PlanetApplicationService } from './planet-application.service';
import { GlobalEventDispatcher } from '../global-event-dispatcher';
import { Router } from '@angular/router';
import { globalPlanet, getPlanetApplicationRef, getApplicationLoader } from '../global-planet';

export enum ApplicationStatus {
assetsLoading = 1,
Expand Down Expand Up @@ -67,8 +68,14 @@ export class PlanetApplicationLoader {
private ngZone: NgZone,
router: Router,
injector: Injector,
private applicationRef: ApplicationRef
applicationRef: ApplicationRef
) {
if (getApplicationLoader()) {
throw new Error(
'PlanetApplicationLoader has been injected in the portal, repeated injection is not allowed'
);
}

this.options = {
switchMode: SwitchModes.default,
errorHandler: (error: Error) => {
Expand Down
32 changes: 4 additions & 28 deletions packages/planet/src/application/planet-application-ref.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { defineApplication, getPlanetApplicationRef } from './planet-application-ref';
import { PlanetPortalApplication } from './portal-application';
import { platformBrowser } from '@angular/platform-browser';
import { NgModule, NgModuleRef, NgModuleFactory, Compiler, Injector, Component, NgZone } from '@angular/core';
import { NgModule, Compiler, Injector, Component, NgZone } from '@angular/core';
import { RouterModule, Router } from '@angular/router';
import { async, TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { defineApplication, getPlanetApplicationRef, clearGlobalPlanet } from '../global-planet';

@Component({
selector: 'app-root',
Expand All @@ -27,30 +26,7 @@ class AppModule {}

describe('PlanetApplicationRef', () => {
afterEach(() => {
// delete all apps
Object.keys(window['planet'].apps).forEach(appName => {
delete window['planet'].apps[appName];
});
});

describe('defineApplication', () => {
it('should define application success', () => {
defineApplication('app1', (portalApp?: PlanetPortalApplication) => {
return new Promise(() => {});
});
expect(window['planet'].apps['app1']).toBeTruthy();
});

it('should throw error when define application has exist', () => {
defineApplication('app1', (portalApp?: PlanetPortalApplication) => {
return new Promise(() => {});
});
expect(() => {
defineApplication('app1', (portalApp?: PlanetPortalApplication) => {
return new Promise(() => {});
});
}).toThrowError('app1 application has exist.');
});
clearGlobalPlanet();
});

describe('getPlanetApplicationRef', () => {
Expand Down
45 changes: 3 additions & 42 deletions packages/planet/src/application/planet-application-ref.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { PlanetRouterEvent, PlanetApplication } from '../planet.class';
import { PlanetApplication } from '../planet.class';
import { PlanetPortalApplication } from './portal-application';
import { NgModuleRef, NgZone, ApplicationRef } from '@angular/core';
import { Router, NavigationEnd, NavigationStart } from '@angular/router';
import { NgModuleRef, NgZone } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { PlantComponentConfig } from '../component/plant-component.config';
import { PlanetComponentRef } from '../component/planet-component-ref';
import { take } from 'rxjs/operators';
import { Observable, from } from 'rxjs';

declare const window: any;
export interface GlobalPlanet {
apps: { [key: string]: PlanetApplicationRef };
registerApps: PlanetApplication[];
portalApplication: PlanetPortalApplication;
}

const globalPlanet: GlobalPlanet = (window.planet = window.planet || {
apps: {},
registerApps: []
});

export type BootstrapAppModule = (portalApp?: PlanetPortalApplication) => Promise<NgModuleRef<any>>;

export type PlantComponentFactory = <TData>(
Expand Down Expand Up @@ -109,30 +97,3 @@ export class PlanetApplicationRef {
}
}
}

export function defineApplication(name: string, bootstrapModule: BootstrapAppModule) {
if (window.planet.apps[name]) {
throw new Error(`${name} application has exist.`);
}
const appRef = new PlanetApplicationRef(name, bootstrapModule);
window.planet.apps[name] = appRef;
}

export function getPlanetApplicationRef(appName: string): PlanetApplicationRef {
const planet = (window as any).planet;
if (planet && planet.apps && planet.apps[appName]) {
return planet.apps[appName];
} else {
return null;
}
}

export function setPortalApplicationData<T>(data: T) {
globalPlanet.portalApplication.data = data;
}

export function getPortalApplicationData<TData>(): TData {
return globalPlanet.portalApplication.data as TData;
}

export { globalPlanet };
21 changes: 11 additions & 10 deletions packages/planet/src/application/planet-application.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { PlanetApplicationService, getPlanetApplicationByName } from './planet-application.service';
import { PlanetApplicationService } from './planet-application.service';
import { SwitchModes } from '../planet.class';
import { HttpClient } from '@angular/common/http';
import { app1, app2, app2WithPreload } from '../test/applications';
import { AssetsLoader } from 'ngx-planet/assets-loader';
import { Planet } from 'ngx-planet/planet';

describe('PlanetApplicationService', () => {
let planetApplicationService: PlanetApplicationService;
Expand All @@ -15,6 +17,14 @@ describe('PlanetApplicationService', () => {
planetApplicationService = TestBed.get(PlanetApplicationService);
});

it(`should repeat injection not allowed`, () => {
window['planet'].applicationService = TestBed.get(PlanetApplicationService);
expect(() => {
return new PlanetApplicationService(TestBed.get(HttpClient), TestBed.get(AssetsLoader));
}).toThrowError('PlanetApplicationService has been injected in the portal, repeated injection is not allowed');
window['planet'].applicationService = null;
});

describe('register', () => {
it('should register signal app1 success', () => {
planetApplicationService.register(app1);
Expand Down Expand Up @@ -154,13 +164,4 @@ describe('PlanetApplicationService', () => {
expect(appsToPreload).toEqual([]);
});
});

describe('getPlanetApplicationByName', () => {
it('should get planet application by name', () => {
planetApplicationService.register(app1);
const app = getPlanetApplicationByName(app1.name);
expect(app).toBe(app1);
expect(getPlanetApplicationByName(app2.name)).toEqual(undefined);
});
});
});
28 changes: 15 additions & 13 deletions packages/planet/src/application/planet-application.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PlanetApplication } from '../planet.class';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { shareReplay, map, switchMap, startWith } from 'rxjs/operators';
import { shareReplay, map } from 'rxjs/operators';
import { coerceArray } from '../helpers';
import { Observable, of } from 'rxjs';
import { AssetsLoadResult, AssetsLoader } from '../assets-loader';
import { globalPlanet } from './planet-application-ref';
import { Observable } from 'rxjs';
import { AssetsLoader } from '../assets-loader';
import { getApplicationService } from '../global-planet';

@Injectable({
providedIn: 'root'
Expand All @@ -15,7 +15,13 @@ export class PlanetApplicationService {

private appsMap: { [key: string]: PlanetApplication } = {};

constructor(private http: HttpClient, private assetsLoader: AssetsLoader) {}
constructor(private http: HttpClient, private assetsLoader: AssetsLoader) {
if (getApplicationService()) {
throw new Error(
'PlanetApplicationService has been injected in the portal, repeated injection is not allowed'
);
}
}

register<TExtra>(appOrApps: PlanetApplication<TExtra> | PlanetApplication<TExtra>[]) {
const apps = coerceArray(appOrApps);
Expand All @@ -26,7 +32,6 @@ export class PlanetApplicationService {
this.apps.push(app);
this.appsMap[app.name] = app;
});
globalPlanet.registerApps = this.apps;
}

registerByUrl(url: string): Observable<void> {
Expand All @@ -50,7 +55,6 @@ export class PlanetApplicationService {
this.apps = this.apps.filter(app => {
return app.name !== name;
});
globalPlanet.registerApps = this.apps;
}
}

Expand All @@ -64,6 +68,10 @@ export class PlanetApplicationService {
});
}

getAppByName(name: string) {
return this.appsMap[name];
}

getAppByMatchedUrl<TExtra>(url: string): PlanetApplication<TExtra> {
return this.getApps().find(app => {
if (app.routerPathPrefix instanceof RegExp) {
Expand All @@ -88,9 +96,3 @@ export class PlanetApplicationService {
return this.apps;
}
}

export function getPlanetApplicationByName(name: string) {
return globalPlanet.registerApps.find(app => {
return app.name === name;
});
}
16 changes: 9 additions & 7 deletions packages/planet/src/component/planet-component-loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ import { TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
import { Compiler, Injector, Type, NgModuleRef } from '@angular/core';
import { app1Name, App1Module, App1ProjectsComponent } from './test/app1.module';
import { app2Name, App2Module } from './test/app2.module';
import { defineApplication, getPlanetApplicationRef } from '../application/planet-application-ref';
import { PlanetPortalApplication } from '../application/portal-application';
import { PlanetComponentLoader } from './planet-component-loader';
import { PlanetApplicationLoader } from '../application/planet-application-loader';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterModule } from '@angular/router';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PlantComponentConfig } from './plant-component.config';
import { defineApplication, getPlanetApplicationRef, getApplicationLoader, clearGlobalPlanet } from '../global-planet';
import { Planet } from 'ngx-planet/planet';

describe('PlanetComponentLoader', () => {
let compiler: Compiler;
let injector: Injector;
let planet: Planet;

function defineAndBootstrapApplication(name: string, appModule: Type<any>) {
const ngModuleFactory = compiler.compileModuleSync(appModule);
Expand All @@ -34,17 +35,18 @@ describe('PlanetComponentLoader', () => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterModule.forRoot([])]
});
});

afterEach(() => {
(window as any).planet.apps = {};
planet = TestBed.get(Planet);
});

beforeEach(inject([Compiler, Injector], (_compiler: Compiler, _injector: Injector) => {
compiler = _compiler;
injector = _injector;
}));

afterEach(() => {
clearGlobalPlanet();
});

it('should register component success', fakeAsync(() => {
// mock app1 and app2 bootstrap
const app1ModuleRef = defineAndBootstrapApplication(app1Name, App1Module);
Expand Down Expand Up @@ -76,7 +78,7 @@ describe('PlanetComponentLoader', () => {
// mock app2 bootstrap
const app2ModuleRef = defineAndBootstrapApplication(app2Name, App1Module);
// mock app1 preload
const applicationLoader = app2ModuleRef.injector.get(PlanetApplicationLoader);
const applicationLoader = getApplicationLoader();
const applicationLoaderSpy = spyOn(applicationLoader, 'preload');
const preload$ = of(getPlanetApplicationRef(app1Name)).pipe(
tap(() => {
Expand Down

0 comments on commit 244f428

Please sign in to comment.