UNPKG

@lotto24-angular/imports-orchestrator

Version:

Orchestrate dynamically imported components in Angular applications

904 lines (870 loc) 40.3 kB
import * as i0 from '@angular/core'; import { EventEmitter, Directive, Output, inject, Renderer2, ElementRef, Input, InjectionToken, Injectable, Injector, runInInjectionContext, ViewContainerRef, NgModuleRef, ChangeDetectorRef, createNgModule, Component, ChangeDetectionStrategy, ViewChild, makeEnvironmentProviders } from '@angular/core'; import { Subscription, Subject, ReplaySubject, takeUntil, share, firstValueFrom, tap, filter, of, shareReplay, map, observeOn, asyncScheduler, startWith, pairwise, mergeMap, skip, race, timeout, catchError, TimeoutError, from, take } from 'rxjs'; import { Router, NavigationStart, NavigationEnd } from '@angular/router'; import { assertPromise, assertSignal, resolvePromiseWithRetries, assertStandalone } from '@lotto24-angular/util'; import { toObservable } from '@angular/core/rxjs-interop'; class ImportsOrchestratorLifecycleDirective { constructor() { this.importQueued = new EventEmitter(); this.importStarted = new EventEmitter(); this.importFinished = new EventEmitter(); this.importComponent = new EventEmitter(); this.importErrored = new EventEmitter(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorLifecycleDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorLifecycleDirective, isStandalone: true, selector: "[importLifecycle]", outputs: { importQueued: "importQueued", importStarted: "importStarted", importFinished: "importFinished", importComponent: "importComponent", importErrored: "importErrored" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorLifecycleDirective, decorators: [{ type: Directive, args: [{ selector: '[importLifecycle]', standalone: true, }] }], propDecorators: { importQueued: [{ type: Output }], importStarted: [{ type: Output }], importFinished: [{ type: Output }], importComponent: [{ type: Output }], importErrored: [{ type: Output }] } }); class ImportsOrchestratorCSSClassDirective { constructor() { this.subscriptions = new Subscription(); this.lifecycle = inject(ImportsOrchestratorLifecycleDirective, { self: true, }); this.subscriptions.add(this.lifecycle.importComponent.subscribe(this.onImportComponent.bind(this))); } onImportComponent(componentRef) { if (!this.cssClass) return; const renderer2 = componentRef.injector.get(Renderer2); const elementRef = componentRef.injector.get(ElementRef); const htmlElement = elementRef.nativeElement; const classes = this.cssClass.match(/[^\s]+/gi); classes?.forEach((c) => renderer2.addClass(htmlElement, c)); } ngOnDestroy() { this.subscriptions.unsubscribe(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorCSSClassDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorCSSClassDirective, isStandalone: true, selector: "[importCSSClass]", inputs: { cssClass: "cssClass" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorCSSClassDirective, decorators: [{ type: Directive, args: [{ selector: '[importCSSClass]', standalone: true, }] }], ctorParameters: () => [], propDecorators: { cssClass: [{ type: Input }] } }); class ImportsOrchestratorIODirective { constructor() { this.destroy$ = new Subject(); this._inputs = new ReplaySubject(1); this.inputs$ = this._inputs.pipe(takeUntil(this.destroy$), share()); this._outputs = new ReplaySubject(1); this.outputs$ = this._outputs.pipe(takeUntil(this.destroy$), share()); } set inputs(value) { this._inputs.next(value ?? {}); } set outputs(value) { this._outputs.next(value ?? {}); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorIODirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorIODirective, isStandalone: true, selector: "[importLifecycle]", inputs: { inputs: "inputs", outputs: "outputs" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorIODirective, decorators: [{ type: Directive, args: [{ selector: '[importLifecycle]', standalone: true, }] }], propDecorators: { inputs: [{ type: Input }], outputs: [{ type: Input }] } }); const IMPORT_PRIORITY_LOWEST = 9999999999; function findImportPriority(priorities, importId, logger) { if (typeof priorities[importId] === 'number') { return priorities[importId]; } const key = Object.keys(priorities).find((key) => importId.startsWith(key)); if (key) { return priorities[key]; } logger.warn(`no priority found for import '${importId}; falling back to lowest priority'`); return IMPORT_PRIORITY_LOWEST; } function findFn(config, key, trail = []) { const stringOrFn = config[key]; if (trail.includes(key)) { throw new Error(`circular reference found: ${[...trail, key].join(' => ')}`); } if (typeof stringOrFn === 'string') { return findFn(config, stringOrFn, [...trail, key]); } if (typeof stringOrFn === 'function') { return stringOrFn; } throw new Error(`Missing entry for key="${key}"`); } /** * recursive loading of queued features */ async function processQueueItem(queue, logger) { // let's take the next item off the queue const item = queue.take(); // let's stop if there are no items in the queue if (!item) { logger.debug('queue is drained'); return; } logger.debug(`queue item resolve (${item})`); try { item.hooks.start.next(item); item.lifecycle?.importStarted?.emit(); const result = await item.resolveFn(item); item.hooks.finish.next(item); item.lifecycle?.importFinished?.emit(result); item.callback && item.callback(result, null); logger.debug(`queue item resolved (${item})`); } catch (x) { item.hooks.error.next([item, x]); item.lifecycle?.importErrored?.emit(x); item.callback && item.callback(null, x); logger.error(`error resolving queue item (${item})`, x); } finally { item.hooks.start.complete(); item.hooks.finish.complete(); item.hooks.error.complete(); } } const IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_CONCURRENCY'); const IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_INTERCEPTOR'); const IMPORTS_ORCHESTRATOR_FEATURE_ROUTING = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_DEFER_UNTIL_FIRST_NAVIGATION'); const IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_TIMEOUT'); const IMPORTS_ORCHESTRATOR_FEATURE_LOGGER = new InjectionToken('IMPORTS_ORCHESTRATOR_FEAUTURE_LOGGER'); const IMPORTS_ORCHESTRATOR_FEATURE_QUEUE = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_QUEUE'); const IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION'); const IMPORTS_STORE = {}; const IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE = new InjectionToken('IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE', { providedIn: 'platform', factory: () => IMPORTS_STORE }); class ImportsQueueProcessor { constructor() { this.logger = inject(IMPORTS_ORCHESTRATOR_FEATURE_LOGGER); this.queue = inject(IMPORTS_ORCHESTRATOR_FEATURE_QUEUE); this.isRoutingActive$ = inject(IMPORTS_ORCHESTRATOR_FEATURE_ROUTING); this.concurrency = inject(IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY); this.running = 0; } static { this.processing = false; } process() { if (!ImportsQueueProcessor.processing) { // do not await, as it would block the lifecycle callback from completing until the queue is processed ImportsQueueProcessor.processing = true; this.processQueue() .then(() => { this.logger.debug('queue processing ended'); }) .catch(() => { this.logger.debug('queue processing failed'); }) .finally(() => { ImportsQueueProcessor.processing = false; }); } } async processQueue() { await this.suspendForNavigation(); const concurrency = this.updateConcurrency(); const concurrentBatch = []; for (let i = this.running; i < concurrency; i++) { this.running++; concurrentBatch.push(this.processItem()); } this.logger.debug(`queue starting ${concurrentBatch.length} item(s) to reach max concurrency (running=${this.running})`); await Promise.all(concurrentBatch); } async processItem() { await processQueueItem(this.queue, this.logger); this.running--; if (!this.queue.empty) { await this.processQueue(); } } updateConcurrency() { const value = typeof this.concurrency === 'function' ? this.concurrency() : this.concurrency; if (value !== this.concurrency) { this.logger.debug(`queue concurrency changed to ${value}`); } return value; } async suspendForNavigation() { // suspend processing while routing, as navigation takes precedence return firstValueFrom(this.isRoutingActive$.pipe(tap((active) => { if (active) { this.logger.debug('queue processing suspended for navigation'); } }), filter((active) => !active))); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsQueueProcessor, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class ImportService { constructor() { this.queueProcessor = inject(ImportsQueueProcessor); this.timeout = inject(IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT); this.logger = inject(IMPORTS_ORCHESTRATOR_FEATURE_LOGGER); this.queue = inject(IMPORTS_ORCHESTRATOR_FEATURE_QUEUE); this.orchestration = inject(IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION); this.interceptor = inject(IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR, { optional: true }); this.injector = inject(Injector); } createQueueItem(identifier, destroy$, options = {}) { const opts = { ...options, injector: options.injector ?? this.injector, timeout: options.timeout ?? this.timeout, }; const imports = this.importsFromDI(opts.injector); const resolveFn = this.resolveFnFromImports(imports, identifier); const priority = findImportPriority(this.orchestration, identifier, this.logger); const hooks = { queued: new Subject(), start: new Subject(), finish: new Subject(), error: new Subject(), }; runInInjectionContext(this.injector, () => { if (this.interceptor) { this.interceptor(identifier, hooks); } }); return { ...opts, priority, identifier, resolveFn, destroy$, hooks, logger: this.logger, toString: () => `@identifier="${identifier}", @priority=${priority}`, }; } async addItemToQueue(item) { const promise = new Promise((resolve, reject) => { item.callback = (result, err) => { if (err) { reject(err); } else { resolve(result); } }; }); this.queue.insert(item.priority, item); item.lifecycle?.importQueued?.emit(); item.hooks.queued.next(item); item.hooks.queued.complete(); this.logger.debug(`queue insert ${item.toString()}`); this.queueProcessor.process(); return promise; } async bypassQueue(item) { this.logger.debug(`bypass queue ${item.toString()}`); return item.resolveFn(item); } removeItemFromQueue(item) { return this.queue.take(item) !== undefined; } importsFromDI(injector) { try { const store = injector.get(IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE); return store; } catch (x) { throw new Error(` Could not inject ${IMPORTS_ORCHESTRATOR_FEATURE_IMPORTS_STORE}. Did you \`provideImports({...})\` in a component or module? If you did, you may need to provide an Injector when calling createQueueItem. ${x}`); } } resolveFnFromImports(imports, identifier) { try { return findFn(imports, identifier); } catch (x) { throw new Error(` Could not find ImportResolveFn. Did you \`provideImports({...})\` in a component or module? If you did, you may need to provide an Injector when calling createQueueItem. ${x}`); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportService, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class ImportsOrchestratorQueueDirective { constructor() { this.io = inject(ImportsOrchestratorIODirective, { self: true, }); this.lifecycle = inject(ImportsOrchestratorLifecycleDirective, { self: true, }); this.destroyComponents$ = new Subject(); this.viewContainerRef = inject(ViewContainerRef); this.moduleRef = inject(NgModuleRef); this.importService = inject(ImportService); this.item = null; } ngOnChanges(changes) { const importInput = changes['import']; if (importInput !== undefined && importInput.currentValue !== importInput.previousValue) { this.createAndAddItemToQueue(); } } createAndAddItemToQueue() { this.removeItemFromQueue(); this.destroyComponents$.next(); // destroy a previously mounted component(s) const injector = Injector.create({ providers: [ ...(this.providers ?? []), { provide: ViewContainerRef, useValue: this.viewContainerRef }, ], parent: this.moduleRef.injector, }); this.item = this.importService.createQueueItem(this.import, this.destroyComponents$, { ...this, injector, lifecycle: this.lifecycle, }); this.importService.addItemToQueue(this.item); } removeItemFromQueue() { if (this.item) { this.importService.removeItemFromQueue(this.item); this.item = null; } } ngOnDestroy() { this.removeItemFromQueue(); this.destroyComponents$.next(); this.destroyComponents$.complete(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorQueueDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorQueueDirective, isStandalone: true, selector: "[importQueue]", inputs: { import: "import", providers: "providers", timeout: "timeout" }, usesOnChanges: true, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorQueueDirective, decorators: [{ type: Directive, args: [{ selector: '[importQueue]', standalone: true, }] }], propDecorators: { import: [{ type: Input }], providers: [{ type: Input }], timeout: [{ type: Input }] } }); var ImportsOrchestratorFeatureKind; (function (ImportsOrchestratorFeatureKind) { ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Logger"] = 0] = "Logger"; ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Timeout"] = 1] = "Timeout"; ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Routing"] = 2] = "Routing"; ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Concurrency"] = 3] = "Concurrency"; ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Interceptor"] = 4] = "Interceptor"; // internal ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Orchestration"] = 5] = "Orchestration"; ImportsOrchestratorFeatureKind[ImportsOrchestratorFeatureKind["Queue"] = 6] = "Queue"; })(ImportsOrchestratorFeatureKind || (ImportsOrchestratorFeatureKind = {})); function importsOrchestratorFeature(kind, providers) { return { kind, providers }; } function withConcurrencyRelativeToDownlinkSpeed(max = 4, min = 1) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY, useValue: downlinkToConcurrencyFn(max, min), }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers); } /** * @param onUpdateConcurrencyFn called when queue processor updates concurrency; */ function withConcurrencyUpdateFn(onUpdateConcurrencyFn) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY, useValue: onUpdateConcurrencyFn, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers); } function withConcurrencyStatic(value) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_CONCURRENCY, useValue: value, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Concurrency, providers); } function downlinkToConcurrencyFn(max, min = 1) { return () => { const downlink = navigator.connection?.downlink ?? 10; if (downlink < 1) { return min; } else if (downlink < 2) { return Math.max(min, Math.floor(min + (max - min) / 4)); } else if (downlink < 4) { return Math.max(min, Math.floor(min + (max - min) / 2)); } else if (downlink < 8) { return Math.max(min, Math.floor(min + (max - min) / 1.5)); } return max; }; } function withInterceptor(interceptor) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_INTERCEPTOR, useValue: interceptor, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Interceptor, providers); } function withSuspendWhileRouting(suspendForInitialNavigation = true) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_ROUTING, useFactory: (router) => isRoutingActive$(router, suspendForInitialNavigation), deps: [Router], }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Routing, providers); } function withoutRouting() { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_ROUTING, useValue: of(false).pipe(shareReplay(1)), }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Routing, providers); } function isRoutingActive$(router, suspendForInitialNavigation) { const navigationActive$ = router.events.pipe(filter((event) => event instanceof NavigationStart || event instanceof NavigationEnd), map((event) => { if (event instanceof NavigationStart) { return true; } return false; }), observeOn(asyncScheduler), startWith(suspendForInitialNavigation)); return navigationActive$.pipe(shareReplay(1)); } function withLogger(logger) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_LOGGER, useValue: logger, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Logger, providers); } function withTimeout(timeout = 10000) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_TIMEOUT, useValue: timeout, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Timeout, providers); } function bindComponentInputs(componentRef, item) { item.io?.inputs$ .pipe(takeUntil(item.destroy$), startWith({}), pairwise(), filter(([_, current]) => Object.keys(current).length > 0), mergeMap(([previous, current]) => Object.entries(current).filter(([key, value]) => previous[key] !== value))) .subscribe(([key, value]) => componentRef.setInput(key, value)); } function bindComponentOutputs(componentRef, item) { const outputs$ = item.io?.outputs$.pipe(share()); if (!outputs$) return; const output$ = outputs$.pipe(takeUntil(item.destroy$), startWith({}), pairwise(), filter(([_, current]) => Object.keys(current).length > 0), mergeMap(([previous, current]) => Object.entries(current).filter(([key, value]) => !previous[key])) // tap(([key, value]) => console.log(key, value)) ); output$.subscribe(([key, value]) => { if (typeof value !== 'function') { throw new Error(`outputs.${key} must be a function, got '${typeof value}'`); } componentRef.instance[key] .pipe(takeUntil(item.destroy$), takeUntil(output$.pipe(filter(([k]) => k === key), skip(1) // skip the first value emitted from replay // tap(() => console.log('unsubscribe componentRef, key=' + key)) ))) .subscribe((data) => { value(data); }); }); } const NG_MOD_DEF = getClosureSafeProperty({ ɵmod: getClosureSafeProperty, }); const ES_MODULE = getClosureSafeProperty({ __esModule: getClosureSafeProperty, }); function isESModule(type) { return type[Symbol.toStringTag] === 'Module' || type[ES_MODULE]; } function isNgModuleDef(type) { return !!type[NG_MOD_DEF]; } function getClosureSafeProperty(objWithPropertyToExtract) { for (const key in objWithPropertyToExtract) { if (objWithPropertyToExtract[key] === getClosureSafeProperty) { return key; } } throw Error('Could not find renamed property on target object.'); } function deferUntilComponentReady$(componentRef, item) { const instance = componentRef.instance; if (!assertImportedComponentReadyEmitter(instance)) { return of(undefined); } item.logger.debug(`deferring until component w/ import=${item.identifier} emits ready`); componentRef.injector.get(ChangeDetectorRef).markForCheck(); // ensure Lifecycle hooks are called return race(resolveReady(instance), item.destroy$).pipe(timeout(item.timeout), catchError((err) => { if (err instanceof TimeoutError) { item.logger.warn(`deferred component w/ import=${item.identifier} timed out after ${item.timeout}ms`); } else { item.logger.error(`deferred component w/ import=${item.identifier} errored: ${err}`); } return of(undefined); })); } function assertImportedComponentReadyEmitter(type) { return type.importedComponentReady !== undefined; } function resolveReady(instance) { const callback = instance.importedComponentReady.call(instance); if (assertPromise(callback)) { return from(callback); } return (assertSignal(callback) ? toObservable(callback) : callback).pipe(filter((value) => value), map(() => undefined), take(1)); } async function mountComponent(componentRef, item) { item.destroy$.subscribe(() => componentRef.destroy()); if (item.io) { bindComponentInputs(componentRef, item); bindComponentOutputs(componentRef, item); } try { await firstValueFrom(deferUntilComponentReady$(componentRef, item)); } catch (x) { // no-op } // This will trigger Angular lifecycle on componentRef's entire component tree // * Bindings will be resolved // * Projected content will be processed // * Usages of ImportsOrchestratorQueueDirective in the tree will then insert items to the queue // * It is of vital importance that items are queued before triggering processQueue again // IMPORTANT: markForCheck is not enough, as it would not cause an immediate change detection cycle componentRef.injector.get(ChangeDetectorRef).detectChanges(); } function isAssumedESModuleContainingAngularComponentsOrModules(type) { return typeof type === 'object' && Object.keys(type).some(key => key.includes('Module') || key.includes('Component')); } function resolveConstructorsFromESModule(esm) { if (isESModule(esm) || isAssumedESModuleContainingAngularComponentsOrModules(esm)) { const constructors = Object.values(esm).filter((v) => typeof v === 'function'); if (!constructors) { throw new Error('Class not found'); } return constructors; } return [esm]; } function importStandalone(promise) { return async (item) => { const resolvedImport = (await resolvePromiseWithRetries(promise)); const constructor = resolveConstructorsFromESModule(resolvedImport).shift(); if (!constructor) { throw new Error('class not found'); } assertStandalone(constructor); const viewContainerRef = item.injector.get(ViewContainerRef); const componentRef = viewContainerRef.createComponent(constructor, { injector: item.injector, }); await mountComponent(componentRef, item); item.lifecycle?.importComponent?.emit(componentRef); return componentRef; }; } function importNgModule(promise) { return async (item) => { const resolvedImport = (await resolvePromiseWithRetries(promise)); const ngModuleConstructors = resolveConstructorsFromESModule(resolvedImport)?.filter((type) => isNgModuleDef(type)); if (ngModuleConstructors?.length > 1) { item.logger.warn(`ES module should return a single NgModule definition, found ${ngModuleConstructors?.length}. Please append \`.then(m => m.{DesiredModule})\` to your dynamic import.`); } const ngModuleConstructor = ngModuleConstructors?.shift(); if (!ngModuleConstructor) { throw new Error('no ngModuleRef constructor found'); } const ngModuleRef = createNgModule(ngModuleConstructor, item.injector); const componentConstructors = ngModuleRef._bootstrapComponents; if (!componentConstructors?.length) { item.logger.debug('no bootstrap components found in ngModuleRef'); item.lifecycle?.importFinished?.emit(undefined); return; } const viewContainerRef = ngModuleRef.injector.get(ViewContainerRef); const mountComponentPromises = componentConstructors.map((componentConstructor) => { const componentRef = viewContainerRef.createComponent(componentConstructor, { injector: ngModuleRef.injector, ngModuleRef }); return mountComponent(componentRef, item).then(() => componentRef); }); const componentRefs = await Promise.all(mountComponentPromises); componentRefs.forEach((componentRef) => item.lifecycle?.importComponent?.emit(componentRef)); return componentRefs; }; } function importPromise(promise) { return async () => resolvePromiseWithRetries(promise); } class ImportsOrchestratorComponent { constructor() { this.queue = inject(ImportsOrchestratorQueueDirective, { self: true, }); } ngOnChanges(changes) { const importInput = changes['import']; if (importInput !== undefined && importInput.currentValue !== importInput.previousValue) { this.createAndAddItemToQueue(); } } ngAfterViewInit() { this.createAndAddItemToQueue(); } createAndAddItemToQueue() { if (!this.identifier || !this.container) { return; } this.queue.viewContainerRef = this.container; this.queue.import = this.identifier; this.queue.createAndAddItemToQueue(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorComponent, isStandalone: true, selector: "import", inputs: { identifier: "identifier" }, viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, read: ViewContainerRef }], usesOnChanges: true, hostDirectives: [{ directive: ImportsOrchestratorQueueDirective, inputs: ["providers", "providers", "timeout", "timeout"] }, { directive: ImportsOrchestratorIODirective, inputs: ["inputs", "inputs", "outputs", "outputs"] }, { directive: ImportsOrchestratorLifecycleDirective, outputs: ["importQueued", "importQueued", "importStarted", "importStarted", "importFinished", "importFinished", "importComponent", "importComponent", "importErrored", "importErrored"] }, { directive: ImportsOrchestratorCSSClassDirective, inputs: ["cssClass", "cssClass"] }], ngImport: i0, template: `<ng-container #container></ng-container>`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorComponent, decorators: [{ type: Component, args: [{ selector: 'import', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, hostDirectives: [ { directive: ImportsOrchestratorQueueDirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['providers', 'timeout'], }, { directive: ImportsOrchestratorIODirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['inputs', 'outputs'], }, { directive: ImportsOrchestratorLifecycleDirective, // eslint-disable-next-line @angular-eslint/no-outputs-metadata-property outputs: [ 'importQueued', 'importStarted', 'importFinished', 'importComponent', 'importErrored', ], }, { directive: ImportsOrchestratorCSSClassDirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['cssClass'], }, ], template: `<ng-container #container></ng-container>`, }] }], propDecorators: { identifier: [{ type: Input }], container: [{ type: ViewChild, args: ['container', { read: ViewContainerRef }] }] } }); const Imports = (imports) => { Object.entries(imports).forEach(([key, value]) => { IMPORTS_STORE[key] = value; }); return function (target) { // noop }; }; class ImportsOrchestratorDirective { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.0.6", type: ImportsOrchestratorDirective, isStandalone: true, selector: "[import]", hostDirectives: [{ directive: ImportsOrchestratorQueueDirective, inputs: ["import", "import", "providers", "providers", "timeout", "timeout"] }, { directive: ImportsOrchestratorIODirective, inputs: ["inputs", "inputs", "outputs", "outputs"] }, { directive: ImportsOrchestratorLifecycleDirective, outputs: ["importQueued", "importQueued", "importStarted", "importStarted", "importFinished", "importFinished", "importComponent", "importComponent", "importErrored", "importErrored"] }, { directive: ImportsOrchestratorCSSClassDirective, inputs: ["cssClass", "cssClass"] }], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: ImportsOrchestratorDirective, decorators: [{ type: Directive, args: [{ selector: '[import]', standalone: true, hostDirectives: [ { directive: ImportsOrchestratorQueueDirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['import', 'providers', 'timeout'], }, { directive: ImportsOrchestratorIODirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['inputs', 'outputs'], }, { directive: ImportsOrchestratorLifecycleDirective, // eslint-disable-next-line @angular-eslint/no-outputs-metadata-property outputs: [ 'importQueued', 'importStarted', 'importFinished', 'importComponent', 'importErrored', ], }, { directive: ImportsOrchestratorCSSClassDirective, // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: ['cssClass'], }, ], }] }] }); class Queue { constructor() { this.data = []; } insert(priority, payload) { this.data = [...this.data, { priority, payload }].sort(comparePriority); } take(payload = null) { if (!payload) { return this.data.shift()?.payload || null; } return this.takeSpecific(payload); } peek() { return this.data[0]?.payload; } get length() { return this.data.length; } get empty() { return this.length < 1; } takeSpecific(payload) { const index = this.data.findIndex((s) => s.payload === payload); if (index < 0) { return null; } payload = this.data[index].payload; const updated = this.data.slice(); updated.splice(index, 1); this.data = updated; return payload; } } function comparePriority(a, b) { return a.priority - b.priority; } function withQueue(queue) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_QUEUE, useValue: queue, }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Queue, providers); } function withOrchestration(orchestration) { const providers = [ { provide: IMPORTS_ORCHESTRATOR_FEATURE_ORCHESTRATION, useFactory: (logger) => validateOrchestration(orchestration, logger), deps: [IMPORTS_ORCHESTRATOR_FEATURE_LOGGER], }, ]; return importsOrchestratorFeature(ImportsOrchestratorFeatureKind.Orchestration, providers); } function validateOrchestration(orchestration, logger) { const conflicts = findConflictingPriorities(orchestration).map(([priority, imports]) => `conflicting priority=${priority} for @imports="${imports.join('", "')}"`); if (conflicts.length > 0) { logger.warn(conflicts.join('\n')); } return orchestration; } function findConflictingPriorities(orchestration) { const byPriority = Object.entries(orchestration).reduce((acc, [key, value]) => { acc[value] = [...(acc[value] || []), key]; return acc; }, {}); return Object.entries(byPriority).filter(([order, imports]) => imports.length > 1); } const provideImportsOrchestration = (orchestration, ...features) => makeEnvironmentProviders([ // default values: ...[ withoutRouting(), withConcurrencyStatic(2), withLogger(console), withTimeout(10000), withQueue(new Queue()), ].map((feature) => feature.providers), // configured values from features ...(features || []).map((feature) => feature.providers), ...withOrchestration(orchestration).providers, ]); /** * Generated bundle index. Do not edit. */ export { ES_MODULE, ImportService, Imports, ImportsOrchestratorCSSClassDirective, ImportsOrchestratorComponent, ImportsOrchestratorDirective, ImportsOrchestratorIODirective, ImportsOrchestratorLifecycleDirective, ImportsOrchestratorQueueDirective, NG_MOD_DEF, bindComponentInputs, bindComponentOutputs, importNgModule, importPromise, importStandalone, isAssumedESModuleContainingAngularComponentsOrModules, isESModule, isNgModuleDef, mountComponent, provideImportsOrchestration, resolveConstructorsFromESModule, withConcurrencyRelativeToDownlinkSpeed, withConcurrencyStatic, withConcurrencyUpdateFn, withInterceptor, withLogger, withSuspendWhileRouting, withTimeout, withoutRouting }; //# sourceMappingURL=lotto24-angular-imports-orchestrator.mjs.map