diff --git a/schematics/src/extension/files/__name@dasherize__/exports/__name@dasherize__-exports.module.__tsext__ b/schematics/src/extension/files/__name@dasherize__/exports/__name@dasherize__-exports.module.__tsext__ index 6703a0a8ac..fc23fd6475 100644 --- a/schematics/src/extension/files/__name@dasherize__/exports/__name@dasherize__-exports.module.__tsext__ +++ b/schematics/src/extension/files/__name@dasherize__/exports/__name@dasherize__-exports.module.__tsext__ @@ -1,15 +1,16 @@ import { NgModule } from '@angular/core'; -import { ReactiveComponentLoaderModule } from '@wishtack/reactive-component-loader'; import { FeatureToggleModule } from 'ish-core/feature-toggle.module'; +import { LAZY_FEATURE_MODULE } from 'ish-core/utils/module-loader/module-loader.service'; @NgModule({ - imports: [ - FeatureToggleModule, - ReactiveComponentLoaderModule.withModule({ - moduleId: 'ish-extensions-<%= dasherize(name) %>', - loadChildren: '../<%= dasherize(name) %>.module#<%= classify(name) %>Module', - }), + imports: [FeatureToggleModule], + providers: [ + { + provide: LAZY_FEATURE_MODULE, + useValue: { feature: '<%= dasherize(name) %>', location: import('../<%= dasherize(name) %>.module') }, + multi: true, + }, ], declarations: [], exports: [], diff --git a/src/app/core/configurations/ngrx-state-transfer.ts b/src/app/core/configurations/ngrx-state-transfer.ts index 2a51c95e15..cf2a4b1466 100644 --- a/src/app/core/configurations/ngrx-state-transfer.ts +++ b/src/app/core/configurations/ngrx-state-transfer.ts @@ -7,7 +7,7 @@ import { mergeDeep } from 'ish-core/utils/functions'; // tslint:disable:no-any -const NGRX_STATE_SK = makeStateKey('ngrxState'); +export const NGRX_STATE_SK = makeStateKey('ngrxState'); const STATE_ACTION_TYPE = '[Internal] Import NgRx State'; /** diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index c30a368fa3..e7444b5a96 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -17,6 +17,7 @@ import { IconModule } from './icon.module'; import { AuthInterceptor } from './interceptors/auth.interceptor'; import { MockInterceptor } from './interceptors/mock.interceptor'; import { StateManagementModule } from './state-management.module'; +import { ModuleLoaderService } from './utils/module-loader/module-loader.service'; export function translateFactory(http: HttpClient) { return new TranslateHttpLoader(http, 'assets/i18n/', '.json'); @@ -70,7 +71,8 @@ export class CoreModule { @Optional() @SkipSelf() parentModule: CoreModule, - popoverConfig: NgbPopoverConfig + popoverConfig: NgbPopoverConfig, + moduleLoader: ModuleLoaderService ) { if (parentModule) { throw new Error('CoreModule is already loaded. Import it in the AppModule only'); @@ -79,5 +81,7 @@ export class CoreModule { popoverConfig.placement = 'top'; popoverConfig.triggers = 'hover'; popoverConfig.container = 'body'; + + moduleLoader.init(); } } diff --git a/src/app/core/store/configuration/configuration.effects.ts b/src/app/core/store/configuration/configuration.effects.ts index 7cabdf349f..2d552c418d 100644 --- a/src/app/core/store/configuration/configuration.effects.ts +++ b/src/app/core/store/configuration/configuration.effects.ts @@ -1,5 +1,6 @@ import { isPlatformBrowser, isPlatformServer } from '@angular/common'; -import { ApplicationRef, Inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { ApplicationRef, Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core'; +import { TransferState } from '@angular/platform-browser'; import { Actions, Effect, ROOT_EFFECTS_INIT, ofType } from '@ngrx/effects'; import { routerNavigationAction } from '@ngrx/router-store'; import { Store, select } from '@ngrx/store'; @@ -19,6 +20,7 @@ import { } from 'rxjs/operators'; import { LARGE_BREAKPOINT_WIDTH, MEDIUM_BREAKPOINT_WIDTH } from 'ish-core/configurations/injection-keys'; +import { NGRX_STATE_SK } from 'ish-core/configurations/ngrx-state-transfer'; import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; import { ConfigurationService } from 'ish-core/services/configuration/configuration.service'; import { distinctCompareWith, mapErrorToAction, mapToProperty, whenFalsy, whenTruthy } from 'ish-core/utils/operators'; @@ -41,6 +43,7 @@ export class ConfigurationEffects { private configService: ConfigurationService, private translateService: TranslateService, private stateProperties: StatePropertiesService, + @Optional() private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: string, private appRef: ApplicationRef, @Inject(MEDIUM_BREAKPOINT_WIDTH) private mediumBreakpointWidth: number, @@ -77,23 +80,26 @@ export class ConfigurationEffects { ); @Effect() - setInitialRestEndpoint$ = this.actions$.pipe( - ofType(ROOT_EFFECTS_INIT), - take(1), - withLatestFrom( - this.stateProperties.getStateOrEnvOrDefault('ICM_BASE_URL', 'icmBaseURL'), - this.stateProperties.getStateOrEnvOrDefault('ICM_SERVER', 'icmServer'), - this.stateProperties.getStateOrEnvOrDefault('ICM_SERVER_STATIC', 'icmServerStatic'), - this.stateProperties.getStateOrEnvOrDefault('ICM_CHANNEL', 'icmChannel'), - this.stateProperties.getStateOrEnvOrDefault('ICM_APPLICATION', 'icmApplication'), - this.stateProperties - .getStateOrEnvOrDefault('FEATURES', 'features') - .pipe(map(x => (typeof x === 'string' ? x.split(/,/g) : x))), - this.stateProperties.getStateOrEnvOrDefault('THEME', 'theme').pipe(map(x => x || 'default')) - ), - map( - ([, baseURL, server, serverStatic, channel, application, features, theme]) => - new ApplyConfiguration({ baseURL, server, serverStatic, channel, application, features, theme }) + setInitialRestEndpoint$ = iif( + () => !this.transferState || !this.transferState.hasKey(NGRX_STATE_SK), + this.actions$.pipe( + ofType(ROOT_EFFECTS_INIT), + take(1), + withLatestFrom( + this.stateProperties.getStateOrEnvOrDefault('ICM_BASE_URL', 'icmBaseURL'), + this.stateProperties.getStateOrEnvOrDefault('ICM_SERVER', 'icmServer'), + this.stateProperties.getStateOrEnvOrDefault('ICM_SERVER_STATIC', 'icmServerStatic'), + this.stateProperties.getStateOrEnvOrDefault('ICM_CHANNEL', 'icmChannel'), + this.stateProperties.getStateOrEnvOrDefault('ICM_APPLICATION', 'icmApplication'), + this.stateProperties + .getStateOrEnvOrDefault('FEATURES', 'features') + .pipe(map(x => (typeof x === 'string' ? x.split(/,/g) : x))), + this.stateProperties.getStateOrEnvOrDefault('THEME', 'theme').pipe(map(x => x || 'default')) + ), + map( + ([, baseURL, server, serverStatic, channel, application, features, theme]) => + new ApplyConfiguration({ baseURL, server, serverStatic, channel, application, features, theme }) + ) ) ); diff --git a/src/app/core/store/configuration/configuration.reducer.ts b/src/app/core/store/configuration/configuration.reducer.ts index ed3626eeff..9095d170c9 100644 --- a/src/app/core/store/configuration/configuration.reducer.ts +++ b/src/app/core/store/configuration/configuration.reducer.ts @@ -28,7 +28,7 @@ const initialState: ConfigurationState = { serverStatic: undefined, channel: undefined, application: undefined, - features: [], + features: undefined, gtmToken: undefined, theme: undefined, locales: environment.locales, diff --git a/src/app/core/store/configuration/configuration.selectors.spec.ts b/src/app/core/store/configuration/configuration.selectors.spec.ts index 2cfd470733..fbdecbcf65 100644 --- a/src/app/core/store/configuration/configuration.selectors.spec.ts +++ b/src/app/core/store/configuration/configuration.selectors.spec.ts @@ -35,7 +35,7 @@ describe('Configuration Selectors', () => { expect(getICMBaseURL(store$.state)).toBeUndefined(); expect(getICMServerURL(store$.state)).toBeUndefined(); expect(getICMStaticURL(store$.state)).toBeUndefined(); - expect(getFeatures(store$.state)).toBeEmpty(); + expect(getFeatures(store$.state)).toBeUndefined(); expect(getGTMToken(store$.state)).toBeUndefined(); expect(isServerConfigurationLoaded(store$.state)).toBeFalsy(); expect(getServerConfigParameter('application.applicationType')(store$.state)).toMatchInlineSnapshot(`undefined`); diff --git a/src/app/core/utils/feature-toggle/feature-toggle.service.ts b/src/app/core/utils/feature-toggle/feature-toggle.service.ts index d5ef3e77ec..bb31294b48 100644 --- a/src/app/core/utils/feature-toggle/feature-toggle.service.ts +++ b/src/app/core/utils/feature-toggle/feature-toggle.service.ts @@ -8,7 +8,7 @@ export class FeatureToggleService { private featureToggles: string[]; constructor(store: Store<{}>) { - store.pipe(select(getFeatures)).subscribe(features => (this.featureToggles = features)); + store.pipe(select(getFeatures)).subscribe(features => (this.featureToggles = features || [])); } enabled(feature: string): boolean { diff --git a/src/app/core/utils/module-loader/module-loader.service.ts b/src/app/core/utils/module-loader/module-loader.service.ts new file mode 100644 index 0000000000..8a20c9bfa3 --- /dev/null +++ b/src/app/core/utils/module-loader/module-loader.service.ts @@ -0,0 +1,62 @@ +import { Compiler, Injectable, InjectionToken, Injector, NgModuleFactory } from '@angular/core'; +import { Store, select } from '@ngrx/store'; + +import { getFeatures } from 'ish-core/store/configuration'; +import { FeatureToggleService } from 'ish-core/utils/feature-toggle/feature-toggle.service'; +import { whenTruthy } from 'ish-core/utils/operators'; + +declare interface LazyModuleType { + feature: string; + // tslint:disable-next-line: no-any + location: any; +} + +export const LAZY_FEATURE_MODULE = new InjectionToken('lazyModule'); + +@Injectable({ providedIn: 'root' }) +export class ModuleLoaderService { + private loadedModules: string[] = []; + + constructor( + private compiler: Compiler, + private injector: Injector, + private featureToggleService: FeatureToggleService, + private store: Store<{}> + ) {} + + init() { + this.store + .pipe( + select(getFeatures), + whenTruthy() + ) + .subscribe(() => { + const lazyModules = this.injector.get(LAZY_FEATURE_MODULE, []); + lazyModules + .filter(mod => !this.loadedModules.includes(mod.feature)) + .filter(mod => this.featureToggleService.enabled(mod.feature)) + .forEach(async mod => { + await this.loadModule(mod.location); + this.loadedModules.push(mod.feature); + }); + }); + } + + private async loadModule(loc) { + const loaded = await loc; + Object.keys(loaded) + .filter(key => key.endsWith('Module')) + .forEach(async key => { + const moduleFactory = await this.loadModuleFactory(loaded[key]); + moduleFactory.create(this.injector); + }); + } + + private async loadModuleFactory(t) { + if (t instanceof NgModuleFactory) { + return t; + } else { + return await this.compiler.compileModuleAsync(t); + } + } +}