From 142ce8ef446c59ffda32312eea666f3509a155ed Mon Sep 17 00:00:00 2001 From: danielwiehl Date: Fri, 26 Nov 2021 11:18:46 +0100 Subject: [PATCH] refactor(platform): consolidate API for configuring the platform This commit eliminates the different ways to configure the platform and removes the separate communication channel for library-internal communication. closes #39 closes #96 BREAKING CHANGE: Consolidation of the API for configuring the platform host introduced a breaking change. The communication protocol between host and client is not affected by this change. - API for loading the platform config via a config loader has been removed; to migrate, load the config before starting the platform; - API for passing an app list to `MicrofrontendPlatform.startHost` has been removed; to migrate, register applications via `MicrofrontendPlatformConfig` object, as follows: `MicrofrontendPlatformConfig.applications`; - manual registration of the host application has been removed as now done implicitly; to migrate: - remove host app from the app list; - configure host privileges via `HostConfig` object, as follows: - `MicrofrontendPlatformConfig.host.scopeCheckDisabled` - `MicrofrontendPlatformConfig.host.intentionCheckDisabled` - `MicrofrontendPlatformConfig.host.intentionRegisterApiDisabled` - specify message delivery timeout in `MicrofrontendPlatformConfig.host.messageDeliveryTimeout`; - provide the host's manifest, if any, via `MicrofrontendPlatformConfig.host.manifest`, either as object literal or as URL; - specify the host's symbolic name in `MicrofrontendPlatformConfig.host.symbolicName`; if not specified, defaults to `host`; - the Activator API can now be disabled by setting the flag `MicrofrontendPlatformConfig.activatorApiDisabled` instead of `PlatformConfig.platformFlags.activatorApiDisabled`; - the interface `ApplicationManifest` has been renamed to `Manifest`; - the micro application must now pass its identity (symbolic name) directly as the first argument, rather than via the options object; - the options object passed to `MicrofrontendPlatform.connectToHost` has been renamed from ` MicroApplicationConfig` to `ConnectOptions` and messaging options are now top-level options; to migrate: - set the flag `MicrofrontendPlatformConnectOptions.connect` instead of `MicroApplicationConfig.messaging.enabled` to control if to connect to the platform host; - specify 'broker discovery timeout' in `MicrofrontendPlatformConnectOptions.brokerDiscoverTimeout` instead of `MicroApplicationConfig.messaging.brokerDiscoverTimeout`; - specify 'message delivery timeout' in `MicrofrontendPlatformConnectOptions.messageDeliveryTimeout` instead of `MicroApplicationConfig.messaging.deliveryTimeout`; ### The following snippets illustrate how a migration could look like: #### Before migration ```typescript const applications: ApplicationConfig[] = [ {symbolicName: 'host', manifestUrl: '/manifest.json'}, // optional {symbolicName: 'app1', manifestUrl: 'http://app1/manifest.json'}, {symbolicName: 'app2', manifestUrl: 'http://app2/manifest.json'}, ]; await MicrofrontendPlatform.startHost(applications, {symbolicName: 'host'}); ``` #### After migration ```typescript await MicrofrontendPlatform.startHost({ host: { symbolicName: 'host', manifest: '/manifest.json' }, applications: [ {symbolicName: 'app1', manifestUrl: 'http://app1/manifest.json'}, {symbolicName: 'app2', manifestUrl: 'http://app2/manifest.json'} ] }); ``` #### After migration if inlining the host manifest ```typescript await MicrofrontendPlatform.startHost({ host: { symbolicName: 'host', manifest: { name: 'Host Application', capabilities: [ // capabilities of the host application ], intentions: [ // intentions of the host application ] } }, applications: [ {symbolicName: 'app1', manifestUrl: 'http://app1/manifest.json'}, {symbolicName: 'app2', manifestUrl: 'http://app2/manifest.json'} ], }); ``` --- .../src/app/app.module.ts | 2 +- .../activator/activator-progress.module.ts | 4 +- .../activator/activator-readiness.module.ts | 6 +- .../app/app-shell/app-shell.component.html | 8 +- .../app/app-shell/app-shell.component.scss | 78 +- .../src/app/app-shell/app-shell.component.ts | 6 +- .../browser-outlet.component.ts | 14 +- .../register-capability.component.ts | 4 +- .../register-intention.component.ts | 4 +- .../src/app/platform-initializer.service.ts | 18 +- .../chapters/configuration/configuration.adoc | 254 +++++- .../platform-configuration.adoc | 198 ----- .../platform-configuration.snippets.ts | 88 -- .../starting-the-platform.snippets.ts | 46 + .../starting-the-platform.adoc | 63 -- .../starting-the-platform.snippets.ts | 39 - .../core-concepts/activator/activator.adoc | 4 +- .../message-interception.adoc | 2 +- .../message-interception.snippets.ts | 12 +- .../embedding-microfrontends/outlet.adoc | 4 +- .../intention-api/intention-api.adoc | 25 +- .../intention-api/intention-api.snippets.ts | 2 +- .../core-concepts/overview/overview.adoc | 2 - .../angular-integration-guide.adoc | 62 +- ...-zone-message-client-decorator.snippets.ts | 13 +- ...start-platform-via-initializer.snippets.ts | 37 +- .../start-platform-via-resolver.snippets.ts | 4 +- .../dev-tools/dev-tools.snippets.ts | 29 +- .../intercepting-host-manifest.adoc | 20 + .../intercepting-host-manifest.snippets.ts | 41 + .../chapters/miscellaneous/miscellaneous.adoc | 1 + .../terminology/host-application.adoc | 2 +- .../chapters/terminology/terminology.adoc | 4 +- .../index.adoc | 2 +- .../getting-started-host-app.md | 109 +-- .../getting-started-products-app.md | 10 +- .../getting-started-shopping-cart-app.md | 20 +- docs/site/installation.md | 130 +-- .../src/spec.util.ts | 5 +- .../src/lib/client/connect-options.ts | 36 + .../src/lib/client/context/context.spec.ts | 33 +- .../client/focus/focus-in-event-dispatcher.ts | 6 +- .../manifest-service.spec.ts | 245 +++--- .../manifest-registry/manifest-service.ts | 25 +- .../platform-manifest-service.ts | 26 - .../lib/client/messaging/broker-gateway.ts | 19 +- .../lib/client/messaging/message-client.ts | 6 +- .../client/messaging/message-handler.spec.ts | 387 +++++---- .../lib/client/messaging/messaging.spec.ts | 804 ++++++++++-------- .../messaging/\311\265intent-client.ts" | 3 +- .../messaging/\311\265message-client.ts" | 3 +- .../lib/client/micro-application-config.ts | 40 - .../mouse-move-event-dispatcher.ts | 6 +- .../mouse-event/mouse-up-event-dispatcher.ts | 6 +- .../src/lib/client/public_api.ts | 2 +- .../named-parameter-substitution.spec.ts | 12 +- .../relative-path-resolver.spec.ts | 2 +- .../lib/host/activator/activator-installer.ts | 8 +- .../src/lib/host/application-config.ts | 52 ++ .../src/lib/host/application-registry.spec.ts | 13 +- .../src/lib/host/application-registry.ts | 13 +- .../src/lib/host/focus/focus-tracker.ts | 13 +- .../host/host-application-config-provider.ts | 43 + .../src/lib/host/host-config.ts | 57 ++ .../src/lib/host/host-manifest-interceptor.ts | 85 ++ .../lib/host/host-platform-app-provider.ts | 55 -- .../src/lib/host/manifest-collector.spec.ts | 41 +- .../src/lib/host/manifest-collector.ts | 59 +- .../manifest-registry.spec.ts | 316 +++---- .../\311\265manifest-registry.ts" | 47 +- .../message-broker/client.registry.spec.ts | 2 +- .../lib/host/message-broker/message-broker.ts | 7 +- .../lib/host/microfrontend-platform-config.ts | 69 ++ .../src/lib/host/platform-config-loader.ts | 27 - .../src/lib/host/platform-config.ts | 120 --- .../src/lib/host/platform-intent-client.ts | 18 - .../src/lib/host/platform-message-client.ts | 20 - .../src/lib/host/platform.constants.ts | 15 - .../src/lib/host/public_api.ts | 6 +- .../src/lib/microfrontend-platform.spec.ts | 57 +- .../src/lib/microfrontend-platform.ts | 341 ++++---- .../src/lib/operators.ts | 4 +- .../src/lib/platform-property-service.ts | 9 +- .../src/lib/platform.model.ts | 16 +- .../src/lib/spec.util.spec.ts | 4 +- .../tsconfig.lib.prod.typedoc.json | 7 +- 86 files changed, 2323 insertions(+), 2234 deletions(-) delete mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.adoc delete mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.snippets.ts create mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform.snippets.ts delete mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.adoc delete mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.snippets.ts create mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.adoc create mode 100644 docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.snippets.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/client/connect-options.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/client/manifest-registry/platform-manifest-service.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/client/micro-application-config.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/host/application-config.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/host/host-application-config-provider.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/host/host-config.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/host/host-manifest-interceptor.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/host-platform-app-provider.ts create mode 100644 projects/scion/microfrontend-platform/src/lib/host/microfrontend-platform-config.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/platform-config-loader.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/platform-config.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/platform-intent-client.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/platform-message-client.ts delete mode 100644 projects/scion/microfrontend-platform/src/lib/host/platform.constants.ts diff --git a/apps/microfrontend-platform-devtools/src/app/app.module.ts b/apps/microfrontend-platform-devtools/src/app/app.module.ts index 1f159462..3a034942 100644 --- a/apps/microfrontend-platform-devtools/src/app/app.module.ts +++ b/apps/microfrontend-platform-devtools/src/app/app.module.ts @@ -108,6 +108,6 @@ export function providePlatformInitializerFn(ngZoneMessageClientDecorator: NgZon }); // Run the microfrontend platform as client app - return zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost({symbolicName: 'devtools'})); + return zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost('devtools')); }; } diff --git a/apps/microfrontend-platform-testing-app/src/app/activator/activator-progress.module.ts b/apps/microfrontend-platform-testing-app/src/app/activator/activator-progress.module.ts index 1c2f0f40..054e6ff4 100644 --- a/apps/microfrontend-platform-testing-app/src/app/activator/activator-progress.module.ts +++ b/apps/microfrontend-platform-testing-app/src/app/activator/activator-progress.module.ts @@ -9,7 +9,7 @@ */ import {NgModule} from '@angular/core'; -import {ACTIVATION_CONTEXT, ActivationContext, ContextService, MessageClient, MicroApplicationConfig} from '@scion/microfrontend-platform'; +import {ACTIVATION_CONTEXT, ActivationContext, APP_IDENTITY, ContextService, MessageClient} from '@scion/microfrontend-platform'; import {RouterModule} from '@angular/router'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -26,7 +26,7 @@ import {Beans} from '@scion/toolkit/bean-manager'; export class ActivatorProgressModule { constructor() { - const symbolicName = Beans.get(MicroApplicationConfig).symbolicName; + const symbolicName = Beans.get(APP_IDENTITY); const randomDelay = 1000 + Math.floor(Math.random() * 3000); // range: [1s, 4s); >=1s to exceed the 'activatorLoadTimeout' timeout of app3 [app3#activatorLoadTimeout=800ms] @see environment.ts console.log(`[testing] Delay the readiness signaling of the app '${symbolicName}' by ${randomDelay}ms.`); diff --git a/apps/microfrontend-platform-testing-app/src/app/activator/activator-readiness.module.ts b/apps/microfrontend-platform-testing-app/src/app/activator/activator-readiness.module.ts index ba5a02e5..1c651d1f 100644 --- a/apps/microfrontend-platform-testing-app/src/app/activator/activator-readiness.module.ts +++ b/apps/microfrontend-platform-testing-app/src/app/activator/activator-readiness.module.ts @@ -9,7 +9,7 @@ */ import {NgModule} from '@angular/core'; -import {ACTIVATION_CONTEXT, ActivationContext, ContextService, MessageClient, MessageHeaders, MicroApplicationConfig} from '@scion/microfrontend-platform'; +import {ACTIVATION_CONTEXT, ActivationContext, APP_IDENTITY, ContextService, MessageClient, MessageHeaders} from '@scion/microfrontend-platform'; import {TestingAppTopics} from '../testing-app.topics'; import {RouterModule} from '@angular/router'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -27,7 +27,7 @@ import {Beans} from '@scion/toolkit/bean-manager'; export class ActivatorReadinessModule { constructor() { - const symbolicName = Beans.get(MicroApplicationConfig).symbolicName; + const symbolicName = Beans.get(APP_IDENTITY); const randomDelay = 1000 + Math.floor(Math.random() * 3000); // range: [1s, 4s); >=1s to exceed the 'activatorLoadTimeout' timeout of app3 [app3#activatorLoadTimeout=800ms] @see environment.ts console.log(`[testing] Delay the readiness signaling of the app '${symbolicName}' by ${randomDelay}ms.`); @@ -43,7 +43,7 @@ export class ActivatorReadinessModule { throw Error('[NullActivatorContextError] Not running in an activator context.'); } - const symbolicName = Beans.get(MicroApplicationConfig).symbolicName; + const symbolicName = Beans.get(APP_IDENTITY); const pingReply = `${symbolicName} [primary: ${activationContext.primary}, X-APP-NAME: ${activationContext.activator.properties['X-APP-NAME']}]`; // Subscribe for ping requests. diff --git a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.html b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.html index 9e4b0677..48f9bb33 100644 --- a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.html +++ b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.html @@ -1,5 +1,8 @@
- {{pageTitle}} +
+ + {{pageTitle}} +
focus-within {{appSymbolicName}} @@ -17,6 +20,3 @@ - - - diff --git a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.scss b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.scss index 154d7192..5aa638b0 100644 --- a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.scss +++ b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.scss @@ -12,48 +12,58 @@ > header { flex: none; - display: grid; + display: flex; margin-bottom: 1.5em; - grid-template-columns: auto repeat(3, max-content); - align-items: start; - column-gap: .25em; + align-items: flex-start; - > span.page-title { - font-size: 1.2rem; - font-weight: bold; + > *:not(:last-child) { + margin-right: .25em; } - > img.banner { - height: 35px; - } + > div.title { + flex: auto; - > .chip.focus-within { - @include sci-toolkit-styles.chip(var(--sci-color-accent), null, var(--sci-color-accent)); - padding: .25em 1.5em; - font-size: 1.1rem; - } + > img.banner { + height: 35px; + } - > .chip.app-name { - @include sci-toolkit-styles.chip(var(--sci-color-accent), var(--sci-color-A50), var(--sci-color-accent)); - padding: .25em 1.5em; - font-size: 1.1rem; - font-weight: bold; + > span.page-title { + font-size: 1.2rem; + font-weight: bold; + } } - > .chip.devtools { - @include sci-toolkit-styles.chip(var(--sci-color-accent), null, var(--sci-color-accent)); - padding: .1em 1em; - font-size: 1rem; - font-weight: bold; - display: flex; - align-items: center; - cursor: pointer; + > span.chip { + flex: none; - > button.toggle { - color: var(--sci-color-primary); + &.focus-within { + @include sci-toolkit-styles.chip(var(--sci-color-accent), null, var(--sci-color-accent)); + padding: .25em 1.5em; + font-size: 1.1rem; + } - &.enabled { - color: var(--sci-color-accent); + &.app-name { + @include sci-toolkit-styles.chip(var(--sci-color-accent), var(--sci-color-A50), var(--sci-color-accent)); + padding: .25em 1.5em; + font-size: 1.1rem; + font-weight: bold; + } + + &.devtools { + @include sci-toolkit-styles.chip(var(--sci-color-accent), null, var(--sci-color-accent)); + padding: .1em 1em; + font-size: 1rem; + font-weight: bold; + display: flex; + align-items: center; + cursor: pointer; + + > button.toggle { + color: var(--sci-color-primary); + + &.enabled { + color: var(--sci-color-accent); + } } } } @@ -68,9 +78,5 @@ } } } - - &:not(.top-window) img.banner { - visibility: hidden; - } } diff --git a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.ts b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.ts index c29d8fd0..9d9fce52 100644 --- a/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.ts +++ b/apps/microfrontend-platform-testing-app/src/app/app-shell/app-shell.component.ts @@ -9,7 +9,7 @@ */ import {Component, HostBinding, OnDestroy} from '@angular/core'; import {asapScheduler, Subject} from 'rxjs'; -import {ContextService, FocusMonitor, IS_PLATFORM_HOST, MicroApplicationConfig, OUTLET_CONTEXT, OutletContext} from '@scion/microfrontend-platform'; +import {APP_IDENTITY, ContextService, FocusMonitor, IS_PLATFORM_HOST, OUTLET_CONTEXT, OutletContext} from '@scion/microfrontend-platform'; import {switchMapTo, takeUntil} from 'rxjs/operators'; import {ActivatedRoute} from '@angular/router'; import {Defined} from '@scion/toolkit/util'; @@ -29,12 +29,10 @@ export class AppShellComponent implements OnDestroy { public pageTitle: string; public isFocusWithin: boolean; public isDevToolsOpened = false; - - @HostBinding('class.top-window') public isPlatformHost = Beans.get(IS_PLATFORM_HOST); constructor() { - this.appSymbolicName = Beans.get(MicroApplicationConfig).symbolicName; + this.appSymbolicName = Beans.get(APP_IDENTITY); this.installFocusWithinListener(); this.installRouteActivateListener(); diff --git a/apps/microfrontend-platform-testing-app/src/app/browser-outlet/browser-outlet.component.ts b/apps/microfrontend-platform-testing-app/src/app/browser-outlet/browser-outlet.component.ts index 3c0cc838..0b053a31 100644 --- a/apps/microfrontend-platform-testing-app/src/app/browser-outlet/browser-outlet.component.ts +++ b/apps/microfrontend-platform-testing-app/src/app/browser-outlet/browser-outlet.component.ts @@ -8,13 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ import {ChangeDetectionStrategy, Component, ElementRef, HostBinding, Injector, Input, ViewChild} from '@angular/core'; -import {ManifestService, OutletRouter, SciRouterOutletElement} from '@scion/microfrontend-platform'; +import {OutletRouter, SciRouterOutletElement} from '@scion/microfrontend-platform'; import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; import {Overlay} from '@angular/cdk/overlay'; import {RouterOutletContextComponent} from '../router-outlet-context/router-outlet-context.component'; import {RouterOutletSettingsComponent} from '../router-outlet-settings/router-outlet-settings.component'; import {ActivatedRoute} from '@angular/router'; import {Beans} from '@scion/toolkit/bean-manager'; +import {environment} from '../../environments/environment'; export const URL = 'url'; @@ -102,11 +103,7 @@ export class BrowserOutletComponent { } private readAppEntryPoints(): AppEndpoint[] { - const apps = Beans.get(ManifestService).applications; - return apps.reduce((endpoints, application) => { - const origin = application.origin; - const symbolicName = application.symbolicName; - + return Object.values(environment.apps).reduce((endpoints, app) => { return endpoints.concat(this._activatedRoute.parent.routeConfig.children .filter(route => !!route.data) .map(route => { @@ -115,11 +112,10 @@ export class BrowserOutletComponent { .reduce((encoded, paramKey) => encoded.concat(`${paramKey}=${matrixParams.get(paramKey)}`), []) .join(';'); return { - url: `${origin}/#/${route.path}${matrixParamsEncoded ? `;${matrixParamsEncoded}` : ''}`, - label: `${symbolicName}: ${route.data['pageTitle']}`, + url: `${app.url}/#/${route.path}${matrixParamsEncoded ? `;${matrixParamsEncoded}` : ''}`, + label: `${app.symbolicName}: ${route.data['pageTitle']}`, }; })); - }, new Array()); } } diff --git a/apps/microfrontend-platform-testing-app/src/app/manifest/register-capability/register-capability.component.ts b/apps/microfrontend-platform-testing-app/src/app/manifest/register-capability/register-capability.component.ts index 0c07b918..63e3aa9e 100644 --- a/apps/microfrontend-platform-testing-app/src/app/manifest/register-capability/register-capability.component.ts +++ b/apps/microfrontend-platform-testing-app/src/app/manifest/register-capability/register-capability.component.ts @@ -9,7 +9,7 @@ */ import {Component} from '@angular/core'; import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; -import {Capability, ManifestObjectFilter, ManifestService, MicroApplicationConfig, ParamDefinition} from '@scion/microfrontend-platform'; +import {APP_IDENTITY, Capability, ManifestObjectFilter, ManifestService, ParamDefinition} from '@scion/microfrontend-platform'; import {SciParamsEnterComponent} from '@scion/toolkit.internal/widgets'; import {Observable} from 'rxjs'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -64,7 +64,7 @@ export class RegisterCapabilityComponent { [APP_SYMBOLIC_NAME]: new FormControl(''), }); - this.capabilities$ = Beans.get(ManifestService).lookupCapabilities$({appSymbolicName: Beans.get(MicroApplicationConfig).symbolicName}); + this.capabilities$ = Beans.get(ManifestService).lookupCapabilities$({appSymbolicName: Beans.get(APP_IDENTITY)}); } public onRegister(): void { diff --git a/apps/microfrontend-platform-testing-app/src/app/manifest/register-intention/register-intention.component.ts b/apps/microfrontend-platform-testing-app/src/app/manifest/register-intention/register-intention.component.ts index 2eb834e0..97a6241d 100644 --- a/apps/microfrontend-platform-testing-app/src/app/manifest/register-intention/register-intention.component.ts +++ b/apps/microfrontend-platform-testing-app/src/app/manifest/register-intention/register-intention.component.ts @@ -9,7 +9,7 @@ */ import {Component} from '@angular/core'; import {FormArray, FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; -import {Intention, ManifestObjectFilter, ManifestService, MicroApplicationConfig} from '@scion/microfrontend-platform'; +import {APP_IDENTITY, Intention, ManifestObjectFilter, ManifestService} from '@scion/microfrontend-platform'; import {SciParamsEnterComponent} from '@scion/toolkit.internal/widgets'; import {Observable} from 'rxjs'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -57,7 +57,7 @@ export class RegisterIntentionComponent { [APP_SYMBOLIC_NAME]: new FormControl(''), }); - this.intentions$ = Beans.get(ManifestService).lookupIntentions$({appSymbolicName: Beans.get(MicroApplicationConfig).symbolicName}); + this.intentions$ = Beans.get(ManifestService).lookupIntentions$({appSymbolicName: Beans.get(APP_IDENTITY)}); } public onRegister(): void { diff --git a/apps/microfrontend-platform-testing-app/src/app/platform-initializer.service.ts b/apps/microfrontend-platform-testing-app/src/app/platform-initializer.service.ts index da1c2e09..912e96d2 100644 --- a/apps/microfrontend-platform-testing-app/src/app/platform-initializer.service.ts +++ b/apps/microfrontend-platform-testing-app/src/app/platform-initializer.service.ts @@ -58,8 +58,8 @@ export class PlatformInitializer implements OnDestroy { this.installMessageInterceptors(); this.installIntentInterceptors(); - // Read the apps from the environment - const apps: ApplicationConfig[] = Object.values(environment.apps).map(app => { + // Read testing apps to be registered from the environment + const testingAppConfigs: ApplicationConfig[] = Object.values(environment.apps).map(app => { return { manifestUrl: `${app.url}/assets/${app.symbolicName}-manifest${manifestClassifier}.json`, activatorLoadTimeout: app.activatorLoadTimeout, @@ -68,9 +68,9 @@ export class PlatformInitializer implements OnDestroy { }; }); - // Load the devtools + // Register devtools app if enabled for this environment if (environment.devtools) { - apps.push(environment.devtools); + testingAppConfigs.push(environment.devtools); } // Log the startup progress (startup-progress.e2e-spec.ts). @@ -88,11 +88,11 @@ export class PlatformInitializer implements OnDestroy { // Run the microfrontend platform as host app await this._zone.runOutsideAngular(() => { return MicrofrontendPlatform.startHost({ - apps: apps, + applications: testingAppConfigs, activatorLoadTimeout: environment.activatorLoadTimeout, + activatorApiDisabled: activatorApiDisabled, properties: Array.from(this._queryParams.keys()).reduce((dictionary, key) => ({...dictionary, [key]: this._queryParams.get(key)}), {}), - platformFlags: {activatorApiDisabled: activatorApiDisabled}, - }, {symbolicName: determineAppSymbolicName()}); + }); }, ); @@ -114,7 +114,7 @@ export class PlatformInitializer implements OnDestroy { }); // Run the microfrontend platform as client app - return this._zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost({symbolicName: determineAppSymbolicName()})); + return this._zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost(getCurrentTestingAppSymbolicName())); } private installMessageInterceptors(): void { @@ -236,7 +236,7 @@ export class PlatformInitializer implements OnDestroy { /** * Identifies the currently running app based on the configured apps in the environment and the current URL. */ -function determineAppSymbolicName(): string { +function getCurrentTestingAppSymbolicName(): string { const application = Object.values(environment.apps).find(app => new URL(app.url).host === window.location.host); if (!application) { throw Error(`[AppError] Application served on wrong URL. Supported URLs are: ${Object.values(environment.apps).map(app => app.url)}`); diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/configuration.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/configuration.adoc index e81785cd..a1e8fb0d 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/configuration.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/configuration.adoc @@ -2,12 +2,254 @@ include::{basedir}/_common.adoc[] [[chapter:configuration]] -== Configuration +== Configuration and Startup -This chapter describes how to start and configure the SCION Microfrontend Platform. +This chapter describes how to configure and start the SCION Microfrontend Platform. -:leveloffset: +1 -include::starting-the-platform/starting-the-platform.adoc[] -include::platform-configuration/platform-configuration.adoc[] -:leveloffset: -1 +[.chapter-toc] +**** +[.chapter-title] +In this Chapter + +- <> +- <> +- <> + +**** +''' + +[[chapter:configuration:starting-the-platform-in-host-application]] +[discrete] +=== Starting the Platform in the Host Application + +The host application starts the platform by calling the `MicrofrontendPlatform.startHost` method and passing the platform's configuration, which contains at minimum the web applications to be registered as micro applications. Registered micro applications can connect to the platform and interact with each other. You can also specify various timeouts and control platform behavior. For a detailed overview of platform and application configuration, see chapter <>. + +The following code snippet illustrates how to start the platform in the host application. +[source,typescript] +---- +include::starting-the-platform.snippets.ts[tags=startHost1] +---- +<1> Lists the micro applications able to connect to the platform to interact with other micro applications. + +As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions. The host manifest can be passed either as an object literal or specified as a URL to load it over the network. + +[source,typescript] +---- +include::starting-the-platform.snippets.ts[tags=startHost2] +---- +<1> Specifies the host manifest. Alternatively, you can pass an URL to the manifest for loading it over the network. +<2> Lists the micro applications able to connect to the platform to interact with other micro applications. + +When starting the platform, it loads the manifests of registered micro applications and installs activator microfrontends, if any. The method for starting the platform host returns a `Promise` that resolves once platform startup completed. You should wait for the Promise to resolve before interacting with the platform. + +[[chapter:configuration:connecting-to-the-host]] +[discrete] +=== Connecting to the Platform from a Micro Application + +A micro application connects to the platform host by invoking the method `MicrofrontendPlatform.connectToHost` and passing its identity as argument. The host does check whether the connecting micro application is qualified to connect, i.e., is registered in the host application under that origin; otherwise, the host will reject the connection attempt. + +The following code snippet illustrates how to connect to the platform in a micro application. + +[source,typescript] +---- +include::starting-the-platform.snippets.ts[tags=connectToHost] +---- + +Optionally, you can pass an options object to control how to connect to the platform host. The method returns a `Promise` that resolves when connected to the platform host. You should wait for the Promise to resolve before interacting with the platform. + +[[chapter:configuration:configuring-the-platform]] +[discrete] +=== Configuring the Platform +You configure the platform by passing following <> when starting the host platform. Besides listing micro applications qualified to connect to the platform, you can specify the manifest of the host application, control platform behavior, declare user-specific properties available to micro applications, and more. + +[[objects::microfrontend-platform-config]] +.Properties of `MicrofrontendPlatformConfig` +[cols="1,4,1,1,5"] +|=== +| Property | Type | Mandatory | Default | Description + +| applications +a| `<>[]` +| yes +| +| Lists the micro applications able to connect to the platform to interact with other micro applications. + +See <> for an overview of the properties. + +| host +a| `<>` +| no +| +| Configures the interaction of the host application with the platform. + +As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions. + +See <> for an overview of the properties. + +| activatorApiDisabled +a| `boolean` +| no +a| `true` +| Controls whether the Activator API is enabled. + +Activating the Activator API enables micro applications to contribute `activator` microfrontends. Activator microfrontends are loaded at platform startup for the entire lifecycle of the platform. An activator is a startup hook for micro applications to initialize or register message or intent handlers to provide functionality. + +| [[objects::microfrontend-platform-config:manifestLoadTimeout]]manifestLoadTimeout +a| `number` +| no +| +| Maximum time (in milliseconds) that the platform waits until the manifest of an application is loaded. + +You can set a different timeout per application via <>. If not set, by default, the browser's HTTP fetch timeout applies. +Consider setting this timeout if, for example, a web application firewall delays the responses of unavailable applications. + +| [[objects::microfrontend-platform-config:activatorLoadTimeout]] activatorLoadTimeout +a| `number` +| no +| +| Maximum time (in milliseconds) for each application to signal readiness. + +If specified and activating an application takes longer, the host logs an error and continues startup. Has no effect for applications which provide no activator(s) or are not configured to signal readiness. +You can set a different timeout per application via <>. + +By default, no timeout is set, meaning that if an app fails to signal readiness, e.g., due to an error, that app would block the host startup process indefinitely. It is therefore recommended to specify a timeout accordingly. + +| properties +a| `Dictionary` +| no +| +| Defines user-defined properties which can be read by micro applications via `PlatformPropertyService`. + +|=== + +The `HostConfig` object is used to configure the interaction of the host application with the platform. + +[[objects::host-config]] +.Properties of `HostConfig` +[cols="1,2,1,1,5"] +|=== +| Property | Type | Mandatory | Default | Description + +| symbolicName +a| `string` +| no +a| `host` +| Symbolic name of the host. + +If not set, `host` is used as the symbolic name of the host. The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens. + +| manifest +a| `string` \| +`<>` +| no +| +| The manifest of the host. + +The manifest can be passed either as an object literal or specified as a URL to be loaded over the network. Providing a manifest lets the host contribute capabilities or declare intentions. + +See <> for an overview of the properties of the manifest. + +| scopeCheckDisabled +a| `boolean` +| no +a| `false` +| Controls whether the host can interact with private capabilities of other micro applications. + +By default, scope check is enabled. Disabling scope check is discouraged. + +| intentionCheckDisabled +a| `boolean` +| no +a| `false` +| Controls whether the host can interact with the capabilities of other apps without having to declare respective intentions. + +By default, intention check is enabled. Disabling intention check is strongly discouraged. + +| intentionRegisterApiDisabled +a| `boolean` +| no +a| `true` +| Controls whether the host can register and unregister intentions dynamically at runtime. + +By default, this API is disabled. Enabling this API is strongly discouraged. + +| messageDeliveryTimeout +a| `number` +| no +a| `10s` +| Maximum time (in milliseconds) that the platform waits to receive dispatch confirmation for messages sent by the host until rejecting the publishing Promise. + +|=== + +The `ApplicationConfig` object is used to describe a micro application to be registered as micro application. Registered micro applications can connect to the platform and interact with each other. + +[[objects::application-config]] +.Properties of `ApplicationConfig` +[cols="1,3,1,1,5"] +|=== +| Property | Type | Mandatory | Default | Description + +| symbolicName +a| `string` +| yes +a| +| Unique symbolic name of this micro application. + +The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens. + +| manifestUrl +a| `string` +| yes +| +| URL to the application manifest. + +See <> for an overview of the properties of the manifest. + +| [[objects::application-config:manifestLoadTimeout]]manifestLoadTimeout +a| `number` +| no +| +| Maximum time (in milliseconds) that the host waits until the manifest for this application is loaded. + +If set, overrides the global timeout as configured in <>. + +| [[objects::application-config:activatorLoadTimeout]]activatorLoadTimeout +a| `number` +| no +| +| Maximum time (in milliseconds) for this application to signal readiness. If activating this application takes longer, the host logs an error and continues startup. + +If set, overrides the global timeout as configured in <>. + +| exclude +a| `boolean` +| no +a| `false` +| Excludes this micro application from registration, e.g. to not register it in a specific environment. + +| scopeCheckDisabled +a| `boolean` +| no +a| `false` +| Controls whether this micro application can interact with private capabilities of other micro applications. + +By default, scope check is enabled. Disabling scope check is discouraged. + +| intentionCheckDisabled +a| `boolean` +| no +a| `false` +| Controls whether this micro application can interact with the capabilities of other apps without having to declare respective intentions. + +By default, intention check is enabled. Disabling intention check is strongly discouraged. + +| intentionRegisterApiDisabled +a| `boolean` +| no +a| `true` +| Controls whether this micro application can register and unregister intentions dynamically at runtime. + +By default, this API is disabled. Enabling this API is strongly discouraged. + +|=== diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.adoc deleted file mode 100644 index da5d5038..00000000 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.adoc +++ /dev/null @@ -1,198 +0,0 @@ -:basedir: ../../.. -include::{basedir}/_common.adoc[] - -[[chapter:platform-configuration]] -== Platform Configuration -This chapter explains how to configure micro applications in the host application. - -[.chapter-toc] -**** -[.chapter-title] -In this Chapter - -- <> -- <> -- <> -- <> -- <> - -**** -''' - -[[chapter:platform-configuration:introduction]] -[discrete] -=== Introduction -When bootstrapping the host application, you need to start the SCION Microfrontend Platform and provide the platform configuration. The config contains a list of web applications, so-called micro applications, and optionally some user-defined properties and platform flags. - -You can pass the configuration either directly when starting the platform, or load it asynchronously using a config loader, e.g., for loading the config over the network. - -[[chapter:platform-configuration:configuring-micro-applications]] -[discrete] -=== Configuring Micro Applications -For a web application to interact with the platform, you need to register it as a micro application. For each web app to register, you pass an `ApplicationConfig` to the platform, containing its name, manifest location, and privileges. - -The following code snippet starts the platform and registers four micro applications. - -[source,typescript] ----- -include::platform-configuration.snippets.ts[tags=provide-app-array] ----- -<1> Starts the platform in the host application. -<2> Registers the _product-catalog-app_ as micro application. -<3> Registers the _shopping-cart-app_ as micro application. -<4> Registers the _checkout-app_ as micro application. -<5> Registers the _customer-review-app_ as micro application. - -As you can see, each web app needs to provide a manifest file which to register in the host application. The `ApplicationConfig` object supports the following properties. - -[[chapter:platform-configuration:application-config]] -.Properties of `ApplicationConfig` -[cols="1,1,1,99"] -|=== -| Property | Type | Mandatory | Description - -| symbolicName -a| `string` -| yes -| Unique symbolic name of this micro application. Choose a short, lowercase name which contains alphanumeric characters and optionally dash characters. - -| manifestUrl -a| `string` -| yes -| URL to the application manifest. - -| manifestLoadTimeout -a| `number` -| no -| Maximum time (in milliseconds) that the host waits for this application to fetch its manifest. + -If set, overrides the global timeout as configured in <>. - -| activatorLoadTimeout -a| `number` -| no -| Maximum time (in milliseconds) that the host waits for this application to signal readiness. + -If set, overrides the global timeout as configured in <>. - -| exclude -a| `boolean` -| no -| Excludes this micro application from registration, e.g. to not register it in a specific environment. - -| scopeCheckDisabled -a| `boolean` -| no -| Sets whether or not this micro application can issue intents to private capabilities of other apps. + -By default, scope check is enabled. Disabling scope check is discouraged. - -| intentionCheckDisabled -a| `boolean` -| no -| Sets whether or not this micro application can look up intentions or issue intents for which it has not declared a respective intention. + -By default, intention check is enabled. Disabling intention check is strongly discouraged. - -| intentionRegisterApiDisabled -a| `boolean` -| no -| Sets whether or not the API to manage intentions is disabled for this micro application. + -By default, this API is disabled. With this API enabled, the application can register and unregister intentions dynamically at runtime. Enabling this API is strongly discouraged. - -|=== - -[[chapter:platform-configuration:configuring-micro-applications-and-platform]] -[discrete] -=== Configuring Micro Applications and the Platform -Instead of providing a list of application configs, you can alternatively pass a `PlatformConfig`. It additionally allows you to enable or disable platform flags and to add user-defined properties. User-defined properties are then also available in micro applications. - -The following code snippet starts the platform and sets the two user-defined properties `auth` and `languages`. - -[source,typescript] ----- -include::platform-configuration.snippets.ts[tags=provide-platform-config] ----- -<1> In the `properties` section you can define user-defined properties in the form of a dictionary with key-value pairs. - -User-defined properties are available in micro applications via `PlatformPropertyService`. The following code snippet illustrates how to read platform properties in a micro application. - -[source,typescript] ----- -include::platform-configuration.snippets.ts[tags=read-platform-properties] ----- -<1> Reads all user-defined properties. -<2> Reads a specific user-defined property. - -The `PlatformConfig` object supports the following properties: - -.Properties of `PlatformConfig` -[cols="1,1,1,99"] -|=== -| Property | Type | Mandatory | Description - -| apps -a| `Array` -| yes -| Defines the micro applications running in the platform. See <> for more information. - -| manifestLoadTimeout -a| `number` -| no -| Maximum time (in milliseconds) for each application to fetch its manifest. + -You can set a different timeout per application via `ApplicationConfig.manifestLoadTimeout`. If not set, by default, the browser's HTTP fetch timeout applies. + -Consider setting this timeout if, for example, a web application firewall delays the responses of unavailable applications. - -| activatorLoadTimeout -a| `number` -| no -| Maximum time (in milliseconds) that the host waits for each application to signal readiness. Has no effect for applications having no activator(s) or are not configured to signal readiness. + -You can set a different timeout per application via `ApplicationConfig.activatorLoadTimeout`. By default, no timeout is set. + -If an app fails to signal its readiness, e.g., due to an error, setting no timeout would cause that app to block the startup process indefinitely. - -| properties -a| `Dictionary` -| no -| Defines user-defined properties which can be read by micro applications via `PlatformPropertyService`. - -| platformFlags -a| `PlatformFlags` -| no -| Platform flags are settings and features that you can enable to change how the platform works. See <> for more information. - -|=== - -[[chapter:platform-configuration:configuring-micro-applications-and-platform-asynchronously]] -[discrete] -=== Configuring Micro Applications and the Platform Asynchronously - -If you are looking for a more flexible way of registering the micro applications, you can alternatively provide a `PlatformConfigLoader`. The `PlatformConfigLoader` allows loading the `PlatformConfig` asynchronously, e.g., over the network. - -The following code snippet implements a loader to fetch the platform config over the network. - -[source,typescript] ----- -include::platform-configuration.snippets.ts[tags=create-platform-config-loader] ----- - -When starting the platform, you pass the `class` symbol of your loader instead of the platform config object, as shown in the following example. - -[source,typescript] ----- -include::platform-configuration.snippets.ts[tags=provide-platform-config-loader] ----- - -[[chapter:platform-configuration:enabling-plaform-features]] -[discrete] -=== Enabling Platform Features -Platform flags are settings and features that you can enable to change how the platform works. You can set platform flags via the `PlatformConfig` object when starting the platform in the host application. - -[[chapter:platform-configuration:platform-flags]] -.Properties of `PlatformFlags` -[cols="1,1,1,99"] -|=== -| Property | Type | Mandatory | Description - -| activatorApiDisabled -a| `boolean` -| no -| Sets whether or not the API to provide application activators is disabled. By default, this API is enabled. - -|=== - diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.snippets.ts deleted file mode 100644 index b7bec8b1..00000000 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/platform-configuration/platform-configuration.snippets.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { MicrofrontendPlatform, PlatformConfig, PlatformConfigLoader, PlatformPropertyService } from '@scion/microfrontend-platform'; -import { Beans } from '@scion/toolkit/bean-manager'; - -{ - // tag::provide-app-array[] - MicrofrontendPlatform.startHost({ // <1> - apps: [ - { - symbolicName: 'product-catalog-app', // <2> - manifestUrl: 'https://product-catalog.webshop.io/manifest.json', - }, - { - symbolicName: 'shopping-cart-app', // <3> - manifestUrl: 'https://shopping-cart.webshop.io/manifest.json', - }, - { - symbolicName: 'checkout-app', // <4> - manifestUrl: 'https://checkout.webshop.io/manifest.json', - }, - { - symbolicName: 'customer-review-app', // <5> - manifestUrl: 'https://customer-review.webshop.io/manifest.json', - }, - ], - }); - // end::provide-app-array[] -} - -{ - // tag::provide-platform-config[] - MicrofrontendPlatform.startHost({ - properties: { // <1> - auth: { - realm: 'shopping app', - url: 'https://sso.webshop.io/auth', - }, - languages: ['en', 'de', 'fr', 'it'], - }, - apps: [ - { - symbolicName: 'product-catalog-app', - manifestUrl: 'https://product-catalog.webshop.io/manifest.json', - }, - { - symbolicName: 'shopping-cart-app', - manifestUrl: 'https://shopping-cart.webshop.io/manifest.json', - }, - { - symbolicName: 'checkout-app', - manifestUrl: 'https://checkout.webshop.io/manifest.json', - }, - { - symbolicName: 'customer-review-app', - manifestUrl: 'https://customer-review.webshop.io/manifest.json', - }, - ], - }); - // end::provide-platform-config[] -} - -{ - // tag::create-platform-config-loader[] - class HttpPlatformConfigLoader implements PlatformConfigLoader { - - public load(): Promise { - return fetch('http://webshop.io/config').then(response => { - if (!response.ok) { - throw Error(`Failed to fetch application config [status=${response.status}]`); - } - return response.json(); - }); - } - } - - // end::create-platform-config-loader[] - - // tag::provide-platform-config-loader[] - MicrofrontendPlatform.startHost(HttpPlatformConfigLoader); - // end::provide-platform-config-loader[] -} - -{ - // tag::read-platform-properties[] - const properties = Beans.get(PlatformPropertyService).properties(); // <1> - const authConfig = Beans.get(PlatformPropertyService).get('auth'); // <2> - // end::read-platform-properties[] -} - diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform.snippets.ts new file mode 100644 index 00000000..35fcd8f8 --- /dev/null +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform.snippets.ts @@ -0,0 +1,46 @@ +import {ApplicationConfig, MicrofrontendPlatform} from '@scion/microfrontend-platform'; + +{ + // tag::startHost1[] + MicrofrontendPlatform.startHost({ + applications: [ // <1> + { + symbolicName: 'product-catalog-app', + manifestUrl: 'https://product-catalog.webshop.io/manifest.json', + }, + // ... some more micro applications + ], + }); + // end::startHost1[] +} + +{ + // tag::startHost2[] + MicrofrontendPlatform.startHost({ + host: { + manifest: { // <1> + name: 'Web Shop (Host)', + capabilities: [ + // capabilities of the host application + ], + intentions: [ + // intentions of the host application + ], + }, + }, + applications: [ // <2> + { + symbolicName: 'product-catalog-app', + manifestUrl: 'https://product-catalog.webshop.io/manifest.json', + }, + // ... some more micro applications + ], + }); + // end::startHost2[] +} + +{ + // tag::connectToHost[] + MicrofrontendPlatform.connectToHost('product-catalog-app'); + // end::connectToHost[] +} diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.adoc deleted file mode 100644 index 72e737f9..00000000 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.adoc +++ /dev/null @@ -1,63 +0,0 @@ -:basedir: ../../.. -include::{basedir}/_common.adoc[] - -[[chapter:starting-the-platform]] -== Starting the Platform - -The platform is started differently in the host application and a micro application. - -[.chapter-toc] -**** -[.chapter-title] -In this Chapter - -- <> -- <> - -**** -''' - -[[chapter:starting-the-platform-in-host-application]] -[discrete] -=== Starting the Platform in the Host Application - -The host app starts the platform using the method `MicrofrontendPlatform.startHost` and passes a list of web applications to be registered as micro applications. The platform then loads the manifests of all registered micro applications and starts platform services such as the message broker for client-side messaging. It further may wait for activators to signal ready. - -The following code snippet shows how to start the platform in the host application. -[source,typescript] ----- -include::starting-the-platform.snippets.ts[tags=startHost1] ----- -<1> Defines the applications to be registered in the platform. -<2> Starts the platform host. - -IMPORTANT: If the host application wants to interact with the micro applications, it also must register itself as a micro application. The host application has no extra privileges compared to other micro applications. - -The following code snippet shows how to start the platform on behalf of a micro application. - -[source,typescript] ----- -include::starting-the-platform.snippets.ts[tags=startHost2] ----- -<1> Defines the applications to be registered in the platform. -<2> Starts the platform host on behalf of the _webshop-app_. - -IMPORTANT: The method for starting the platform host returns a `Promise` that resolves when the platform started successfully and activators, if any, signaled ready, or that rejects if the startup fails. You should wait for the Promise to resolve before interacting with the platform. - -See the chapter <> for more information about application registration. - -[[chapter:starting-the-platform-in-micro-application]] -[discrete] -=== Starting the Platform in a Micro Application - -A micro application connects to the platform host by invoking the method `MicrofrontendPlatform.connectToHost` and passing its identity as argument. The host does check whether the connecting micro application is authorized to connect, i.e., is registered in the host platform; otherwise, the host will reject the connection attempt. - -The following code snippet shows how to start the platform in a micro application, e.g. when displaying a microfrontend. - -[source,typescript] ----- -include::starting-the-platform.snippets.ts[tags=connectToHost] ----- - -IMPORTANT: The method for connecting to the platform host returns a `Promise` that resolves when connected to the platform host, or that rejects if not finding the platform host or if the micro application is not authorized to connect. You should wait for the Promise to resolve before interacting with the platform. - diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.snippets.ts deleted file mode 100644 index f9b2e5cc..00000000 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/configuration/starting-the-platform/starting-the-platform.snippets.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ApplicationConfig, MicrofrontendPlatform } from '@scion/microfrontend-platform'; - -{ - // tag::startHost1[] - const apps: ApplicationConfig[] = [ // <1> - { - symbolicName: 'product-catalog-app', - manifestUrl: 'https://product-catalog.webshop.io/manifest.json', - }, - // ... and some more micro applications - ]; - - MicrofrontendPlatform.startHost(apps); // <2> - // end::startHost1[] -} - -{ - // tag::startHost2[] - const apps: ApplicationConfig[] = [ - { - symbolicName: 'webshop-app', // <1> - manifestUrl: 'https://webshop.io/manifest.json', - }, - { - symbolicName: 'product-catalog-app', - manifestUrl: 'https://product-catalog.webshop.io/manifest.json', - }, - // ... and some more micro applications - ]; - - MicrofrontendPlatform.startHost(apps, {symbolicName: 'webshop-app'}); // <2> - // end::startHost2[] -} - -{ - // tag::connectToHost[] - MicrofrontendPlatform.connectToHost({symbolicName: 'product-catalog-app'}); - // end::connectToHost[] -} diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/activator/activator.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/activator/activator.adoc index 2edb9cbd..3df7a312 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/activator/activator.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/activator/activator.adoc @@ -24,7 +24,7 @@ In this Chapter [[chapter:activator:what-is-an-activator]] [discrete] === What is an Activator? -An activator allows a micro application to initialize and connect to the platform when the user loads the host application into his browser. In the broadest sense, an activator is a kind of microfrontend, i.e. an HTML page that runs in an iframe. In contrast to regular microfrontends, however, at platform startup, the platform loads activator microfrontends into hidden iframes for the entire platform lifecycle, thus, providing a stateful session to the micro application on the client-side. +An activator allows a micro application to initialize and connect to the platform when the user loads the host application into his browser. An activator is a startup hook for micro applications to initialize or register message or intent handlers to provide functionality. In the broadest sense, an activator is a kind of microfrontend, i.e. an HTML page that runs in an iframe. In contrast to regular microfrontends, however, at platform startup, the platform loads activator microfrontends into hidden iframes for the entire platform lifecycle, thus, providing a stateful session to the micro application on the client-side. A micro application registers an activator as public _activator_ capability in its manifest, as follows: @@ -69,7 +69,7 @@ Starting an activator may take some time. In order not to miss any messages or i For this purpose, you can define a set of topics where to publish a ready message to signal readiness. If you specify multiple topics, the activator enters ready state after you have published a ready message to all these topics. A ready message is an event; thus, a message without payload. -If not specifying a readiness topic, the platform host does not wait for this activator to become ready. However, if you specify a readiness topic, make sure that your activator has a fast startup time and signals readiness as early as possible not to delay the startup of the platform host. Optionally, you can configure a maximum time that the host waits for an application to signal readiness. For more information, refer to chapter <>. +If not specifying a readiness topic, the platform host does not wait for this activator to become ready. However, if you specify a readiness topic, make sure that your activator has a fast startup time and signals readiness as early as possible not to delay the startup of the platform host. Optionally, you can configure a <> that the host waits for an application to signal readiness. For more information, refer to chapter <>. [source,json] ---- diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.adoc index 79b2f94e..431d9bb9 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.adoc @@ -33,7 +33,7 @@ include::message-interception.snippets.ts[tags=message-logger-interceptor] [[chapter:message-interception:registering-interceptors]] [discrete] === Registering Interceptors -You register interceptors with the bean manager when the host application starts. Interceptors can be registered only in the host application. They are invoked in registration order. +You register interceptors with the bean manager when starting the host application. Interceptors can be registered only in the host application. They are invoked in registration order. [source,typescript] ---- diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.snippets.ts index bdfdb815..7067700a 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/cross-application-communication/message-interception.snippets.ts @@ -1,5 +1,5 @@ -import { Handler, IntentInterceptor, IntentMessage, MessageInterceptor, MicrofrontendPlatform, PlatformConfig, PlatformConfigLoader, PlatformState, TopicMatcher, TopicMessage } from '@scion/microfrontend-platform'; -import { Beans } from '@scion/toolkit/bean-manager'; +import {Handler, IntentInterceptor, IntentMessage, MessageInterceptor, MicrofrontendPlatform, PlatformState, TopicMatcher, TopicMessage} from '@scion/microfrontend-platform'; +import {Beans} from '@scion/toolkit/bean-manager'; { // tag::message-logger-interceptor[] @@ -27,12 +27,6 @@ import { Beans } from '@scion/toolkit/bean-manager'; // end::message-logger-interceptor[] - class YourPlatformConfigLoader implements PlatformConfigLoader { - public load(): Promise { - return undefined; - } - } - // tag::message-logger-interceptor-registration[] MicrofrontendPlatform.whenState(PlatformState.Starting).then(() => { Beans.register(MessageInterceptor, {useClass: MessageLoggerInterceptor, multi: true}); // <1> @@ -40,7 +34,7 @@ import { Beans } from '@scion/toolkit/bean-manager'; }); // Start the platform. - MicrofrontendPlatform.startHost(YourPlatformConfigLoader); // <3> + MicrofrontendPlatform.startHost(...); // <3> // end::message-logger-interceptor-registration[] } diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/embedding-microfrontends/outlet.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/embedding-microfrontends/outlet.adoc index 0e1b8c1a..815cea70 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/embedding-microfrontends/outlet.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/embedding-microfrontends/outlet.adoc @@ -58,12 +58,12 @@ The following figure shows a microfrontend that embeds another microfrontend. [.text-center] image::nested-router-outlets.svg[] -TIP: If using the <>, you do not need to know the URL of the microfrontend you want to embed. Simply issue an intent for showing the microfrontend, pass the outlet name along with the intent, and let the microfrontend embedding itself. See chapter <> for an example. +TIP: If using the <>, you do not need to know the URL of the microfrontend you want to embed. Simply issue an intent for showing the microfrontend, pass the outlet name along with the intent, and let the microfrontend to embed itself. See chapter <> for an example. [[chapter:router-outlet:outlet-size]] [discrete] === Outlet size -The router outlet can adapt its size to the preferred size of its embedded content. The preferred size is set by the microfrontend embedded in the router outlet, which, therefore, requires the embedded microfrontend to be connected to the platform. For detailed instructions on how to register a micro application and connect to the plaform, refer to the chapter <>. +The router outlet can adapt its size to the preferred size of its embedded content. The preferred size is set by the microfrontend embedded in the router outlet, which, therefore, requires the embedded microfrontend to be connected to the platform. For detailed instructions on how to register a micro application and connect to the plaform, refer to the chapter <>. NOTE: The preferred size of an element is the minimum size that allows it to display normally. Setting a preferred size is useful if the outlet is displayed in a layout that aligns its items based on the items' content size. diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.adoc index b050e763..a12f94d8 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.adoc @@ -49,8 +49,9 @@ The following code snippet shows the manifest of the _Product Catalog Applicatio include::intention-api.snippets.ts[tags=manifest] ---- -.Properties of `ApplicationManifest` -[cols="1,1,1,99"] +[[objects::manifest]] +.Properties of `Manifest` +[cols="1,2,1,5"] |=== | Property | Type | Mandatory | Description @@ -66,12 +67,12 @@ a| `string` For a _Single Page Application_ that uses _hash-based_ routing, you typically specify the hash symbol (`#`) as the base URL. | capabilities -a| `Capability[]` +a| `<>[]` | no | Functionality that qualified micro application can look up or call via intent. | intentions -a| `Intention[]` +a| `<>[]` | no | Functionality which this micro application intends to use. |=== @@ -98,9 +99,9 @@ The following code snippet shows an example of how to declare a capability in th include::intention-api.snippets.ts[tags=capability-declaration] ---- -[[chapter:intention-api:capability-properties]] +[[objects::capability]] .Properties of `Capability` -[cols="1,1,1,99"] +[cols="1,2,1,5"] |=== | Property | Type | Mandatory | Description @@ -176,9 +177,9 @@ The following code snippet shows an example of how to declare an intention in th include::intention-api.snippets.ts[tags=intention-declaration] ---- -[[chapter:intention-api:intention-properties]] +[[objects::intention]] .Properties of `Intention` -[cols="1,1,1,99"] +[cols="1,2,1,5"] |=== | Property | Type | Mandatory | Description @@ -255,7 +256,7 @@ By passing a `ManifestObjectFilter` to the `lookupCapabilities$` method, you can IMPORTANT: A micro application can only look up its own capabilities and public capabilities for which it has declared an intention. .Properties of `ManifestObjectFilter` to filter capabilities -[cols="1,1,1,99"] +[cols="1,2,1,8"] |=== | Property | Type | Mandatory | Description @@ -292,7 +293,7 @@ The platform allows you to look up and observe intentions. Unlike when < Registers the capability to inform the user about planned maintenance. <2> Unregisters the capability after 30 seconds. -For an overview of the supported capability properties, see chapter <>. +For an overview of the supported capability properties, see chapter <>. TIP: Capabilities are typically registered in an activator. An activator is a special microfrontend that a micro application can provide to interact with the platform. Activators are loaded when starting the host application and run for the entire application lifecycle. For more information, refer to chapter <>. @@ -370,5 +371,5 @@ include::intention-api.snippets.ts[tags=enable-intention-register-api] ---- <1> Enables the API for the `Product Catalog Application`. -Similar to <>, you can register an intention using the `ManifestService` and its `registerIntention` method. For an overview of the supported intention properties, see chapter <>. +Similar to <>, you can register an intention using the `ManifestService` and its `registerIntention` method. For an overview of the supported intention properties, see chapter <>. diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.snippets.ts index ab88d0a2..eb966e17 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/intention-api/intention-api.snippets.ts @@ -173,7 +173,7 @@ import { Beans } from '@scion/toolkit/bean-manager'; { // tag::enable-intention-register-api[] MicrofrontendPlatform.startHost({ - apps: [ + applications: [ { symbolicName: 'product-catalog-app', manifestUrl: 'https://product-catalog.webshop.io/manifest.json', diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/overview/overview.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/overview/overview.adoc index 995bed0d..84a94668 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/overview/overview.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/core-concepts/overview/overview.adoc @@ -56,7 +56,5 @@ include::{terminologydir}/host-application.adoc[] The host application is the topmost application for integrating microfrontends. It starts the platform host and registers the micro applications. -If the host application wants to interact with either the platform or the micro applications, the host application has to register itself as a micro application. The host application has no extra privileges compared to other micro applications. - NOTE: It is conceivable - although rare - to have more than one host app in the <>. As a prerequisite, every host app must be unique in its window hierarchy, i.e., must not be embedded (nor directly nor indirectly) by another host app. Each host app forms a separate and completely isolated namespace. diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-integration-guide.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-integration-guide.adoc index 9b5ade11..9bfec210 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-integration-guide.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-integration-guide.adoc @@ -12,7 +12,8 @@ In this Chapter - <> - <> -- <> +- <> +- <> - <> - <> - <> @@ -24,10 +25,6 @@ In this Chapter [[chapter:angular-integration-guide:configuring-hash-based-routing]] [discrete] === Configuring Hash-Based Routing -==== -TIP: Read chapter <> to learn more about why to prefer hash-based routing. -==== - We recommend using hash-based routing over HTML 5 push-state routing in micro applications. To enable hash-based routing in an Angular application, pass `{useHash: true}` when configuring the router in `AppRoutingModule`, as follows. [source,typescript] @@ -35,14 +32,15 @@ We recommend using hash-based routing over HTML 5 push-state routing in micro ap include::angular-integration-guide.snippets.ts[tags=configure-hash-based-routing] ---- -[[chapter:angular-integration-guide:starting-platform-in-app-initializer]] -[discrete] -=== Starting the Platform in an Angular App Initializer ==== -TIP: Refer to chapter <> for detailed instructions on how to start the platform in the host and micro applications. +TIP: Read chapter <> to learn more about why to prefer hash-based routing. ==== -The platform should be started during the bootstrapping of the Angular application, that is, before displaying content to the user. For an Angular host application and also for Angular micro applications, we recommend to start the the platform in an app initializer. +[[chapter:angular-integration-guide:starting-platform-in-app-initializer]] +[discrete] +=== Starting the Platform Host in an Angular App Initializer + +The platform should be started during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend starting the platform in an app initializer. See chapter <> for an alternative approach. NOTE: Angular allows hooking into the process of initialization by providing an initializer to the `APP_INITIALIZER` injection token. Angular will wait until all initializers resolved to start the application, making it the ideal place for starting the SCION Microfrontend Platform. @@ -64,49 +62,47 @@ Next, we implement the `PlatformInitializer` to start the platform. ---- include::start-platform-via-initializer.snippets.ts[tags=host-app:initializer] ---- -<1> Injects the Angular `HttpClient` and `NgZone`. -<2> Registers the Angular `HttpClient` as bean in the bean manager, allowing the `HttpPlatformConfigLoader` to load the platform config over the network. -<3> Starts the platform host. It's important to start the platform outside Angular's zone in order to avoid triggering change detection for unrelated DOM events. +<1> Injects the Angular `NgZone`. +<2> Declares or loads the platform config, e.g., using `HttpClient`. +<3> Starts the platform host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles. -And finally, in the code snippet below, the `HttpPlatformConfigLoader` to load the platform config over the network. -[source,typescript] ----- -include::start-platform-via-initializer.snippets.ts[tags=host-app:platform-config-loader] ----- -<1> Fetches the config over the network. Because the loader is constructed by the platform and not Angular, constructor injection is not available. Instead, we can look up the `HttpClient` via the bean manager. -''' +==== +TIP: Refer to chapter <> for detailed instructions on how to start the platform in the host and micro applications. +==== -As for the host application, a micro application can also start the platform via an Angular app initializer, as illustrated by the code snippet below. For reasons of simplicity, we have inlined the platform startup. +[[chapter:angular-integration-guide:connecting-to-host-in-app-initializer]] +[discrete] +=== Connecting to the Host in an Angular App Initializer +A micro application should connect to the platform host during the bootstrapping of the Angular application, that is, before displaying content to the user. Hence, we recommend connecting to the platform host in an app initializer. See chapter <> for an alternative approach. [source,typescript] ---- include::start-platform-via-initializer.snippets.ts[tags=micro-app:initializer] ---- -<1> Starts the platform in the name of the `product-catalog-app` micro application. It's important to start the platform outside Angular's zone in order to avoid triggering change detection for unrelated DOM events. +<1> Provides an initializer to the `APP_INITIALIZER` injection token. +<2> Instructs Angular to pass the `NgZone` to the higher order function. +<3> Returns the initializer function, which connects to the host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles. -[[chapter:angular-integration-guide:starting-platform-in-route-resolver]] +[[chapter:angular-integration-guide:using-route-resolver-instead-app-initializer]] [discrete] -=== Starting the Platform in an Angular Route Resolver -==== -TIP: Refer to chapter <> for detailed instructions on how to start the platform in the host and micro applications. -==== - -If you cannot use an initializer for whatever reason, an alternative would be to use a route resolver. +=== Using a Route Resolver instead of an App Initializer +If you cannot use an app initializer for starting the platform or connecting to the platform host, an alternative would be to use a route resolver. NOTE: Angular allows installing resolvers on a route, allowing data to be resolved asynchronously before the route is finally activated. -The following code snippet refers to starting the platform in a micro application. However, the same applies to starting the platform in the host application. +The following code snippet illustrates how to connect to the platform host using an Angular resolver. Similarly, you could start the platform in the host application. -For a micro application, the resolver implementation could look similar to the following: +For a micro application, the resolver implementation could look as following: [source,typescript] ---- include::start-platform-via-resolver.snippets.ts[tags=resolver] ---- -<1> Starts the platform in the `resolve` method, returning a Promise that resolves when the platform started. It's important to start the platform outside Angular's zone in order to avoid triggering change detection for unrelated DOM events. +<1> Injects the Angular `NgZone`. +<2> Connects to the platform host. It is super important to start the platform outside the Angular zone to avoid excessive change detection cycles. -It is absolutely essential for an application instance to start the platform only once. Therefore, it is recommended to install the resolver in a parent route common to all microfrontend routes. When loading a microfrontend for the first time, Angular will wait activating the child route until the platform finished starting. When navigating to another microfrontend of the micro application, the resolver would not resolve anew. +Ensure that a micro application instance connects to the host only once. Therefore, it is recommended to install the resolver in a parent route common to all microfrontend routes. When loading a microfrontend for the first time, Angular will wait activating the child route until the platform finished starting. When navigating to another microfrontend of the micro application, the resolver would not resolve anew. [source,typescript] ---- diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-zone-message-client-decorator.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-zone-message-client-decorator.snippets.ts index 9fb634ff..8296f414 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-zone-message-client-decorator.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/angular-zone-message-client-decorator.snippets.ts @@ -1,9 +1,8 @@ -import { Intent, IntentClient, IntentMessage, IntentOptions, IntentSelector, MessageClient, MicrofrontendPlatform, PlatformState, PublishOptions, RequestOptions, TopicMessage } from '@scion/microfrontend-platform'; -import { Injectable, NgZone } from '@angular/core'; -import { MonoTypeOperatorFunction, Observable, pipe, Subscription } from 'rxjs'; -import { HttpPlatformConfigLoader } from './start-platform-via-initializer.snippets'; -import { BeanDecorator, Beans } from '@scion/toolkit/bean-manager'; -import { observeInside, subscribeInside } from '@scion/toolkit/operators'; +import {Intent, IntentClient, IntentMessage, IntentOptions, IntentSelector, MessageClient, MicrofrontendPlatform, PlatformState, PublishOptions, RequestOptions, TopicMessage} from '@scion/microfrontend-platform'; +import {Injectable, NgZone} from '@angular/core'; +import {MonoTypeOperatorFunction, Observable, pipe, Subscription} from 'rxjs'; +import {BeanDecorator, Beans} from '@scion/toolkit/bean-manager'; +import {observeInside, subscribeInside} from '@scion/toolkit/operators'; // tag::message-client-decorator[] /** @@ -104,7 +103,7 @@ export class PlatformInitializer { }); // Start the platform. - return this._zone.runOutsideAngular(() => MicrofrontendPlatform.startHost(HttpPlatformConfigLoader)); + return this._zone.runOutsideAngular(() => MicrofrontendPlatform.startHost(...)); } } diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-initializer.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-initializer.snippets.ts index 0fbad3c7..813b13b3 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-initializer.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-initializer.snippets.ts @@ -1,36 +1,23 @@ -import { APP_INITIALIZER, Injectable, NgModule, NgZone } from '@angular/core'; -import { MicrofrontendPlatform, PlatformConfig, PlatformConfigLoader, PlatformState } from '@scion/microfrontend-platform'; -import { HttpClient } from '@angular/common/http'; -import { Beans } from '@scion/toolkit/bean-manager'; +import {APP_INITIALIZER, Injectable, NgModule, NgZone} from '@angular/core'; +import {MicrofrontendPlatform, MicrofrontendPlatformConfig} from '@scion/microfrontend-platform'; // tag::host-app:initializer[] @Injectable({providedIn: 'root'}) export class PlatformInitializer { - constructor(private _httpClient: HttpClient, private _zone: NgZone) { // <1> + constructor(private _zone: NgZone) { // <1> } public init(): Promise { - // Initialize the platform to run with Angular. - MicrofrontendPlatform.whenState(PlatformState.Starting).then(() => { - Beans.register(HttpClient, {useValue: this._httpClient}); // <2> - }); + const config: MicrofrontendPlatformConfig = ...; // <2> - // Start the platform in host-mode. - return this._zone.runOutsideAngular(() => MicrofrontendPlatform.startHost(HttpPlatformConfigLoader)); // <3> + // Start the platform outside of the Angular zone. + return this._zone.runOutsideAngular(() => MicrofrontendPlatform.startHost(config)); // <3> } } // end::host-app:initializer[] -// tag::host-app:platform-config-loader[] -export class HttpPlatformConfigLoader implements PlatformConfigLoader { - public load(): Promise { - return Beans.get(HttpClient).get('assets/platform-config.json').toPromise(); // <1> - } -} -// end::host-app:platform-config-loader[] - // tag::host-app:register-initializer[] @NgModule({ providers: [ @@ -49,6 +36,7 @@ export class AppModule { export function providePlatformInitializerFn(initializer: PlatformInitializer): () => Promise { return (): Promise => initializer.init(); // <3> } + // end::host-app:register-initializer[] // tag::micro-app:initializer[] @@ -56,17 +44,18 @@ export function providePlatformInitializerFn(initializer: PlatformInitializer): providers: [ { provide: APP_INITIALIZER, - useFactory: provideMicroAppPlatformInitializerFn, + useFactory: provideConnectToHostFn, // <1> + deps: [NgZone], // <2> multi: true, - deps: [NgZone], }, ], // ... other metadata omitted }) -export class MicroAppModule { +export class AppModule { } -export function provideMicroAppPlatformInitializerFn(zone: NgZone): () => Promise { - return () => zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost({symbolicName: 'product-catalog-app'})); // <1> +export function provideConnectToHostFn(zone: NgZone): () => Promise { + return () => zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost('')); // <3> } + // end::micro-app:initializer[] diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-resolver.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-resolver.snippets.ts index 9352a0df..045b7af2 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-resolver.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/angular-integration-guide/start-platform-via-resolver.snippets.ts @@ -6,11 +6,11 @@ import { ActivatedRouteSnapshot, Resolve, RouterModule, RouterStateSnapshot, Rou @Injectable({providedIn: 'root'}) export class PlatformInitializer implements Resolve { - constructor(private _zone: NgZone) { + constructor(private _zone: NgZone) { // <1> } public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise { - return this._zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost({symbolicName: 'product-catalog-app'})); // <1> + return this._zone.runOutsideAngular(() => MicrofrontendPlatform.connectToHost('')); // <2> } } diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/dev-tools/dev-tools.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/dev-tools/dev-tools.snippets.ts index 920b581b..1fe91cfb 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/dev-tools/dev-tools.snippets.ts +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/dev-tools/dev-tools.snippets.ts @@ -1,27 +1,32 @@ -` +import {MicrofrontendPlatform, OutletRouter} from '@scion/microfrontend-platform'; +import {Beans} from '@scion/toolkit/bean-manager'; + +{ // tag::dev-tools:register-dev-tools[] - await MicrofrontendPlatform.startHost({ - apps: [ - // your apps + MicrofrontendPlatform.startHost({ + applications: [ + // register your micro application(s) here + + // register the 'devtools' micro application { symbolicName: 'devtools', - manifestUrl: 'https://scion-microfrontend-platform-devtools-.vercel.app/assets/manifest.json', <1> - intentionCheckDisabled: true, <2> - scopeCheckDisabled: true, <2> + manifestUrl: 'https://scion-microfrontend-platform-devtools-.vercel.app/assets/manifest.json', // <1> + intentionCheckDisabled: true, // <2> + scopeCheckDisabled: true, // <2> }, ], }); // end::dev-tools:register-dev-tools[] -`; +} ` // tag::dev-tools:dev-tools-outlet[] - + // end::dev-tools:dev-tools-outlet[] `; -` +{ // tag::dev-tools:dev-tools-navigation[] - Beans.get(OutletRouter).navigate('https://scion-microfrontend-platform-devtools-.vercel.app', {outlet: 'DEV-TOOLS'}); + Beans.get(OutletRouter).navigate('https://scion-microfrontend-platform-devtools-.vercel.app', {outlet: 'devtools'}); // end::dev-tools:dev-tools-navigation[] -`; +} diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.adoc new file mode 100644 index 00000000..8c2c60af --- /dev/null +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.adoc @@ -0,0 +1,20 @@ +:basedir: ../../.. + +[[chapter:intercepting-host-manifest]] +== Intercepting Host Manifest +When starting the platform in the host application, the host can pass a manifest to express its intentions and provide functionality in the form of capabilities. If integrating the platform in a library, you may need to intercept the manifest of the host in order to introduce library-specific behavior. For this reason, the platform provides a hook to intercept the manifest of the host before it is registered with the platform. + +You can register a host manifest interceptor in the bean manager, as following: +[source,typescript] +---- +include::intercepting-host-manifest.snippets.ts[tags=register-interceptor] +---- + +The following interceptors adds an intention and capability to the host manifest. +[source,typescript] +---- +include::intercepting-host-manifest.snippets.ts[tags=interceptor] +---- +<1> Registers an intention in the host manifest, e.g., an intention to open microfrontends. +<2> Registers a capability in the host manifest, e.g., a capability for displaying a message box. + diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.snippets.ts b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.snippets.ts new file mode 100644 index 00000000..eb08a44f --- /dev/null +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/intercepting-host-manifest/intercepting-host-manifest.snippets.ts @@ -0,0 +1,41 @@ +import {Capability, HostManifestInterceptor, Intention, Manifest} from '@scion/microfrontend-platform'; +import {Beans} from '@scion/toolkit/bean-manager'; + +{ + // tag::interceptor[] + class YourInterceptor implements HostManifestInterceptor { + + public intercept(hostManifest: Manifest): void { + hostManifest.intentions = [ + ...hostManifest.intentions || [], + provideMicrofrontendIntention(), // <1> + ]; + hostManifest.capabilities = [ + ...hostManifest.capabilities || [], + provideMessageBoxCapability(), // <2> + ]; + } + } + + function provideMicrofrontendIntention(): Intention { + return { + type: 'microfrontend', + qualifier: {'*': '*'}, + }; + } + + function provideMessageBoxCapability(): Capability { + return { + type: 'messagebox', + qualifier: {}, + private: false, + description: 'Allows displaying a simple message to the user.', + }; + } + + // end::interceptor[] + + // tag::register-interceptor[] + Beans.register(HostManifestInterceptor, {useClass: YourInterceptor, multi: true}); + // end::register-interceptor[] +} diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/miscellaneous.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/miscellaneous.adoc index 2b1d91ca..91d891b5 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/miscellaneous.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/miscellaneous/miscellaneous.adoc @@ -8,6 +8,7 @@ This part of the developer guide contains additional information that may be hel :leveloffset: +1 include::platform-lifecycle/platform-lifecycle.adoc[] include::platform-startup-progress/platform-startup-progress.adoc[] +include::intercepting-host-manifest/intercepting-host-manifest.adoc[] include::focus-monitor/focus-monitor.adoc[] include::routing-in-micro-applications/routing-in-micro-applications.adoc[] include::bean-manager/bean-manager.adoc[] diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/host-application.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/host-application.adoc index 2de6ab34..d64c9a28 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/host-application.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/host-application.adoc @@ -1,3 +1,3 @@ -- -The host application, sometimes also called the container application, provides the top-level integration container for <>. Typically, it is the web app which the user loads into his browser and provides the main application shell, defining areas to embed <>. +The host application, sometimes also called the container application, provides the top-level integration container for <>. Typically, it is the web app which the user loads into his browser that provides the main application shell, defining areas to embed <>. -- diff --git a/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/terminology.adoc b/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/terminology.adoc index 4ce701ec..b17f4bf4 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/terminology.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/chapters/terminology/terminology.adoc @@ -6,7 +6,7 @@ include::{basedir}/_common.adoc[] [#terminology:activator] Activator:: -An activator allows a <> to initialize and connect to the <> when the user loads the <> into his browser. +An activator is a startup hook for micro applications to initialize or register message or intent handlers to provide functionality. Activator microfrontends are loaded at platform startup for the entire lifecycle of the platform + In the broadest sense, an activator is a kind of <>, i.e. an HTML page that runs in an iframe. In contrast to regular <>, however, at platform startup, the <> loads activator microfrontends into hidden iframes for the entire platform lifecycle, thus, providing a stateful session to the <> on the client-side. @@ -35,7 +35,7 @@ Host Application:: + include::host-application.adoc[] + -The host app starts the <> host and registers <>. If the host app wants to interact with either the <> or the <>, the host app has to register itself as a <>. The host app has no extra privileges compared to other <>. +The host app starts the <> host and registers <>. As with micro applications, the host can provide a manifest to contribute behavior. [#terminology:intent] Intent:: diff --git a/docs/adoc/microfrontend-platform-developer-guide/index.adoc b/docs/adoc/microfrontend-platform-developer-guide/index.adoc index e28ec1e8..c1328c3f 100644 --- a/docs/adoc/microfrontend-platform-developer-guide/index.adoc +++ b/docs/adoc/microfrontend-platform-developer-guide/index.adoc @@ -58,8 +58,8 @@ include::{chaptersdir}/scion-microfrontend-platform/scion-microfrontend-platform include::{chaptersdir}/microfrontend-architecture/microfrontend-architecture.adoc[] include::{chaptersdir}/technology/technology.adoc[] include::{chaptersdir}/core-concepts/core-concepts.adoc[] +include::{chaptersdir}/configuration/configuration.adoc[] include::{chaptersdir}/miscellaneous/miscellaneous.adoc[] include::{chaptersdir}/security/security.adoc[] -include::{chaptersdir}/configuration/configuration.adoc[] include::{chaptersdir}/terminology/terminology.adoc[] diff --git a/docs/site/getting-started/getting-started-host-app.md b/docs/site/getting-started/getting-started-host-app.md index cd01fa67..bedeaf54 100644 --- a/docs/site/getting-started/getting-started-host-app.md +++ b/docs/site/getting-started/getting-started-host-app.md @@ -5,7 +5,7 @@ ## [SCION Microfrontend Platform][menu-home] > [Getting Started][menu-getting-started] > Host Application -The host application provides the top-level integration container for microfrontends. It is the web app which the user loads into his browser and provides the main application shell, defining areas to embed microfrontends. +The host application provides the top-level integration container for microfrontends. It is the web app which the user loads into his browser that provides the main application shell, defining areas to embed microfrontends. *** - **Project directory:**\ @@ -44,32 +44,27 @@ Follow the following instructions to get the host application running. Registration of the micro applications
-In this section, we will register the `host`, `products`, `shopping cart` and `devtools` as micro applications and start the platform host. Registered micro applications can interact with the platform and other micro applications. +In this section, we will register the `products`, `shopping cart` and `devtools` web applications as micro applications and start the platform host. Registered micro applications can interact with the platform and other micro applications. 1. Open the TypeScript file `host-controller.ts`. -1. Configure the micro applications by adding the following content before the constructor: - ```ts - import { ApplicationConfig } from '@scion/microfrontend-platform'; - - private platformConfig: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: '/manifest.json'}, - {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, - {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, - {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, - ]; - ``` - For each micro application, we provide an application config with the application's symbolic name and the URL to its manifest. Symbolic names must be unique and are used by the micro applications to connect to the platform host. The manifest is a JSON file that contains information about a micro application. -1. Next, start the platform and register the micro applications by adding the following content to the `init` method, as follows: +2. Start the platform and register the micro applications by adding the following content to the `init` method: ```ts import { MicrofrontendPlatform } from '@scion/microfrontend-platform'; public async init(): Promise { - [+] await MicrofrontendPlatform.startHost(this.platformConfig, {symbolicName: 'host-app'}); - } + [+] await MicrofrontendPlatform.startHost({ + [+] applications: [ + [+] {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + [+] {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + [+] {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + [+] ], + [+] }); + } ``` + > Lines to be added are preceded by the [+] mark. - - The second argument is the symbolic name of the micro application starting the platform host. It is optional. If specified, we can interact with the platform and other micro applications, e.g., publish messages or navigate in router outlets. + + As the argument to `MicrofrontendPlatform.startHost` we pass the configuration of the platform, which contains at minimum the web applications to register as micro applications. Registered micro applications can connect to the platform and interact with each other. Each application, we assign a unique symbolic name and specify its manifest. The symbolic name is used by the micro application to connect to the platform. The manifest is a special file that contains information about the micro application, such as capabilities that the application provides or intentions that the application has.
@@ -98,12 +93,18 @@ In this section, we will embed the `products`, `shopping cart` and `devtools` mi 1. Now, we want to route the primary router outlet to display the `products` microfrontend, as follows: ```ts - import { OutletRouter } from '@scion/microfrontend-platform'; + import { MicrofrontendPlatform, OutletRouter } from '@scion/microfrontend-platform'; import { Beans } from '@scion/toolkit/bean-manager'; public async init(): Promise { // Start the platform - await MicrofrontendPlatform.startHost(this.platformConfig, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + applications: [ + {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + ], + }); [+] // Display the `products` microfrontend in the primary router outlet [+] Beans.get(OutletRouter).navigate('http://localhost:4201/products.html'); @@ -116,7 +117,8 @@ In this section, we will embed the `products`, `shopping cart` and `devtools` mi In the constructor, add a click listener to the shopping cart button and invoke the method `onToggleShoppingCart`, as follows: ```ts - import { MessageClient } from '@scion/microfrontend-platform'; + import { MessageClient, MicrofrontendPlatform, OutletRouter } from '@scion/microfrontend-platform'; + import { Beans } from '@scion/toolkit/bean-manager'; constructor() { document.querySelector('button.shopping-cart').addEventListener('click', () => this.onToggleShoppingCart()); @@ -127,19 +129,25 @@ In this section, we will embed the `products`, `shopping cart` and `devtools` mi Beans.get(MessageClient).publish('shopping-cart/toggle-side-panel'); } ``` - - Unlike to embedding the `products` microfrontend, we publish a message to show the `shopping cart` microfrontend. As of now, nothing would happen when the user clicks on that button, because we did not register a message listener yet. It is important to understand that the platform transports that message to all micro applications. Later, when implementing the `shopping cart` micro application, we will subscribe to such messages and navigate accordingly. Of course, we could also use the `OutletRouter` directly. For illustrative purposes, however, we use an alternative approach, which further has the advantage that we do not have to know the URL of the microfrontend to embed it. Instead, we let the providing micro application perform the routing, keeping the microfrontend URL an implementation detail of the micro application that provides the microfrontend. + + For illustration purposes, unlike to embedding the `products` microfrontend, we publish a message to show the `shopping cart` microfrontend. As of now, nothing would happen when the user clicks on that button, because we did not register a message listener yet. It is important to understand that the platform transports that message to all micro applications. Later, when implementing the `shopping cart` micro application, we will subscribe to such messages and navigate accordingly. Of course, we could also use the `OutletRouter` directly. For illustrative purposes, however, we use an alternative approach, which further has the advantage that we do not have to know the URL of the microfrontend to embed it. Instead, we let the providing micro application perform the routing, keeping the microfrontend URL an implementation detail of the micro application that provides the microfrontend. > Note: It would be even better to use the Intention API for showing a microfrontend, which, however, would go beyond the scope of this Getting Started Guide. For more information, refer to the [Developer Guide][link-developer-guide#routing-in-the-activator]. 1. Finally, we want to route the devtools router outlet to display the `devtools` microfrontend, as follows: ```ts - import { OutletRouter } from '@scion/microfrontend-platform'; + import { MessageClient, MicrofrontendPlatform, OutletRouter } from '@scion/microfrontend-platform'; import { Beans } from '@scion/toolkit/bean-manager'; public async init(): Promise { // Start the platform - await MicrofrontendPlatform.startHost(this.platformConfig, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + applications: [ + {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + ], + }); // Display the `products` microfrontend in the primary router outlet Beans.get(OutletRouter).navigate('http://localhost:4201/products.html'); @@ -166,27 +174,6 @@ sci-router-outlet[name="SHOPPING-CART"].sci-empty { ```
-
- Provide a manifest file -
- -In this step, we finally provide the manifest JSON file that we referenced in the first step. If not providing a manifest file, we could not connect to the platform. - -Create the file `manifest.json` in the `src` folder, as follows: -```json -{ - "name": "Host App" -} -``` - -The manifest must declare at least the human-readable name of the application. The name has no meaning to the platform, but is used, for example, by the DevTools to list the micro applications. - -To learn more about the manifest, refer to the [Developer Guide][link-developer-guide#manifest]. - -> This step requires to serve the application anew. - -
-
Open the app in the browser
@@ -232,25 +219,24 @@ We have added two router outlets to the HTML template of the host application fo The host-controller.ts looks as following: ```ts -import { ApplicationConfig, MessageClient, MicrofrontendPlatform, OutletRouter } from '@scion/microfrontend-platform'; +import { MessageClient, MicrofrontendPlatform, OutletRouter } from '@scion/microfrontend-platform'; import { Beans } from '@scion/toolkit/bean-manager'; class HostController { - private platformConfig: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: '/manifest.json'}, - {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, - {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, - {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, - ]; - constructor() { document.querySelector('button.shopping-cart').addEventListener('click', () => this.onToggleShoppingCart()); } public async init(): Promise { // Start the platform - await MicrofrontendPlatform.startHost(this.platformConfig, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + applications: [ + {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + {symbolicName: 'devtools', manifestUrl: 'https://scion-microfrontend-platform-devtools.vercel.app/assets/manifest.json', intentionCheckDisabled: true, scopeCheckDisabled: true}, + ], + }); // Display the products microfrontend in the primary router outlet Beans.get(OutletRouter).navigate('http://localhost:4201/products.html'); @@ -269,17 +255,6 @@ new HostController().init(); ```
-
- The manifest.json looks as following: - -```json -{ - "name": "Host App" -} - -``` -
-
diff --git a/docs/site/getting-started/getting-started-products-app.md b/docs/site/getting-started/getting-started-products-app.md index 472e054f..b3e0c733 100644 --- a/docs/site/getting-started/getting-started-products-app.md +++ b/docs/site/getting-started/getting-started-products-app.md @@ -52,7 +52,7 @@ In this section, we will connect the `products` micro application to the platfor import { MicrofrontendPlatform } from '@scion/microfrontend-platform'; public async init(): Promise { - [+] await MicrofrontendPlatform.connectToHost({symbolicName: 'products-app'}); + [+] await MicrofrontendPlatform.connectToHost('products-app'); } ``` > Lines to be added are preceded by the [+] mark. @@ -63,7 +63,7 @@ In this section, we will connect the `products` micro application to the platfor Create the file `manifest.json` in the `src` folder, as follows: ```json { - "name": "Products App" + "name": "Products Application" } ``` @@ -122,7 +122,7 @@ In this section, we will render products in an unordered list. ```ts public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'products-app'}); + await MicrofrontendPlatform.connectToHost('products-app'); [+] // Render the products [+] this.products.forEach(product => this.renderProduct(product)); @@ -199,7 +199,7 @@ class ProductsController { public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'products-app'}); + await MicrofrontendPlatform.connectToHost('products-app'); // Render the products this.products.forEach(product => this.renderProduct(product)); @@ -239,7 +239,7 @@ interface Product { ```json { - "name": "Products App" + "name": "Products Application" } ```
diff --git a/docs/site/getting-started/getting-started-shopping-cart-app.md b/docs/site/getting-started/getting-started-shopping-cart-app.md index 2f50a9ca..8621c4fd 100644 --- a/docs/site/getting-started/getting-started-shopping-cart-app.md +++ b/docs/site/getting-started/getting-started-shopping-cart-app.md @@ -52,7 +52,7 @@ In this section, we will connect the `shopping cart` micro application to the pl import { MicrofrontendPlatform } from '@scion/microfrontend-platform'; public async init(): Promise { - [+] await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + [+] await MicrofrontendPlatform.connectToHost('shopping-cart-app'); } ``` > Lines to be added are preceded by the [+] mark. @@ -63,7 +63,7 @@ In this section, we will connect the `shopping cart` micro application to the pl Create the file `manifest.json` in the `src` folder, as follows: ```json { - "name": "Shopping Cart App" + "name": "Shopping Cart Application" } ``` @@ -103,7 +103,7 @@ In this section, we will render the products added to the shopping cart in an un public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + await MicrofrontendPlatform.connectToHost('shopping-cart-app'); [+] // Render products added to the shopping cart [+] ShoppingCartService.products$.subscribe(products => { @@ -204,7 +204,7 @@ Let us register an activator: 1. Register the activator in your manifest.json file, as following: ```ts { - "name": "Shopping Cart App", + "name": "Shopping Cart Application", [+] "capabilities": [{ [+] "type": "activator", [+] "private": false, @@ -232,7 +232,7 @@ Like a regular microfrontend, an activator must connect to the platform host to import { MicrofrontendPlatform } from '@scion/microfrontend-platform'; public async init(): Promise { - [+] await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + [+] await MicrofrontendPlatform.connectToHost('shopping-cart-app'); } ``` > Lines to be added are preceded by the [+] mark. @@ -257,7 +257,7 @@ In this section, we will listen for messages published to the topic `shopping-ca public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + await MicrofrontendPlatform.connectToHost('shopping-cart-app'); [+] // Listener to add a product to the shopping cart [+] Beans.get(MessageClient) @@ -295,7 +295,7 @@ If we recall the implementation of the host application, we notice that we have public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + await MicrofrontendPlatform.connectToHost('shopping-cart-app'); // Listener to add a product to the shopping cart Beans.get(MessageClient) @@ -374,7 +374,7 @@ class ShoppingCartController { public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + await MicrofrontendPlatform.connectToHost('shopping-cart-app'); // Render products added to the shopping cart ShoppingCartService.products$.subscribe(products => { @@ -421,7 +421,7 @@ class Activator { public async init(): Promise { // Connect to the platform host - await MicrofrontendPlatform.connectToHost({symbolicName: 'shopping-cart-app'}); + await MicrofrontendPlatform.connectToHost('shopping-cart-app'); // Listener to add a product to the shopping cart Beans.get(MessageClient) @@ -457,7 +457,7 @@ new Activator().init(); ```json { - "name": "Shopping Cart App", + "name": "Shopping Cart Application", "capabilities": [{ "type": "activator", "private": false, diff --git a/docs/site/installation.md b/docs/site/installation.md index 722b3aec..f35b996c 100644 --- a/docs/site/installation.md +++ b/docs/site/installation.md @@ -7,13 +7,12 @@ This short manual helps to install the SCION Microfrontend Platform and describes how to start the platform. For more detailed instructions, please refer to the [Developer Guide][link-developer-guide#configuration]. - 1. **Install `SCION Microfrontend Platform` using the NPM command-line tool** ```console npm install @scion/microfrontend-platform --save ``` - + 1. **Install `SCION Toolkit` using the NPM command-line tool** ```console @@ -29,72 +28,77 @@ This short manual helps to install the SCION Microfrontend Platform and describe
Start the platform in the host application
- - The host application provides the top-level integration container for microfrontends. Typically, it is the web app which the user loads into his browser and provides the main application shell, defining areas to embed microfrontends. The host application registers the micro applications when starting the platform host. - - 3.1. *Registering micro applications* - - For each micro application to register, you must provide an application config with the application's symbolic name and the URL to its manifest. - ```ts - const platformConfig: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: '/manifest.json'}, // optional - {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, - {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, - ]; - ``` - Symbolic names must be unique and are used by the micro applications to connect to the platform host. The manifest is a JSON file that contains information about a micro application. - - 3.2. *Starting the platform* - - When starting the platform, you pass the app config array as first argument, as following: - ```ts - await MicrofrontendPlatform.startHost(platformConfig, {symbolicName: 'host-app'}); - ``` - Alternatively, you could load the config asynchronously using a config loader, e.g., for loading the config over the network. - - The second argument is the symbolic name of the micro application starting the platform host. It is optional. If specified, the host app can interact with the platform and other micro applications, e.g., publish messages or navigate in router outlets. The host application has no extra privileges compared to other micro applications and must also provide a manifest file. The manifest declares at least the name of the application, as follows: - - ```json - { - "name": "Host App" - } - ``` - - The method for starting the platform host returns a Promise that resolves when the platform started successfully and activators, if any, signaled ready. You should wait for the Promise to resolve before interacting with the platform. -
- + + The host application provides the top-level integration container for microfrontends. Typically, it is the web app which the user loads into his browser that provides the main application shell, defining areas to embed microfrontends. + + The host application starts the platform by invoking the method `MicrofrontendPlatform.startHost` and passing a config with the web applications to register as micro applications. Registered micro applications can interact with the platform and other micro applications. + + ```ts + await MicrofrontendPlatform.startHost({ + applications: [ + {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + ], + }); + ``` + + For each micro application to register, you must provide an application config with the application's symbolic name and the URL to its manifest. Symbolic names must be unique and are used by the micro applications to connect to the platform host. The manifest is a JSON file that contains information about the micro application. + + As with micro applications, the host can provide a manifest to contribute behavior, as following: + + ```ts + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + // capabilities of the host application + ], + intentions: [ + // intentions of the host application + ], + } + }, + applications: [ + {symbolicName: 'products-app', manifestUrl: 'http://localhost:4201/manifest.json'}, + {symbolicName: 'shopping-cart-app', manifestUrl: 'http://localhost:4202/manifest.json'}, + ], + }); + ``` + + The method for starting the platform host returns a Promise that resolves once platform startup completed. You should wait for the Promise to resolve before interacting with the platform. + +
- Start the platform in micro applications + Connect to the platform host in a micro application
- - For a micro application to connect to the platform host, it must be registered in the host application. For this, the micro application must provide a manifest file. - - 3.1. *Providing a manifest* - - Create the manifest file, for example, `manifest.json`. The manifest declares at least the name of the application. - - ```json - { - "name": "Products App" - } - ``` - - 3.2. *Connecting to the platform host* - - ```ts - await MicrofrontendPlatform.connectToHost({symbolicName: 'products-app'}); - ``` - - As the symbolic name, you must pass the exact same name under which you registered the micro application in the host application. - - The method for connecting to the platform host returns a Promise that resolves when connected to the platform host, or that rejects if not finding the platform host or if the micro application is not authorized to connect. You should wait for the Promise to resolve before interacting with the platform. - + + For a micro application to connect to the platform host, it needs to provide a manifest file and be registered in the host application. + + Create the manifest file, for example, `manifest.json`. The manifest declares at minimum the name of the application. + + ```json + { + "name": "Products Application" + } + ``` + + A micro application connects to the platform host by invoking the method `MicrofrontendPlatform.connectToHost` and passing its identity as argument. The host does check whether the connecting micro application is qualified to connect, i.e., is registered in the host application under that origin; otherwise, the host will reject the connection attempt. + + ```ts + await MicrofrontendPlatform.connectToHost('products-app'); + ``` + + As the symbolic name, you must pass the exact same name under which you registered the micro application in the host application. + + The method for connecting to the platform host returns a Promise that resolves when connected to the platform host. You should wait for the Promise to resolve before interacting with the platform. +
- + *** - + For Angular applications, we recommend starting the platform in an app initializer and synchronizing the message client with the Angular zone. For more detailed information on integrating the SCION Microfrontend Platform into an Angular application, please refer to the [Angular Integration Guide][link-developer-guide#angular_integration_guide]. - + [menu-home]: /README.md [menu-projects-overview]: /docs/site/projects-overview.md diff --git a/projects/scion/microfrontend-platform.e2e/src/spec.util.ts b/projects/scion/microfrontend-platform.e2e/src/spec.util.ts index 0aa6ac3f..bdd27288 100644 --- a/projects/scion/microfrontend-platform.e2e/src/spec.util.ts +++ b/projects/scion/microfrontend-platform.e2e/src/spec.util.ts @@ -19,10 +19,13 @@ export async function isCssClassPresent(elementFinder: ElementFinder, cssClass: } /** - * Returns css classes on given element. + * Returns CSS classes on given element. */ export async function getCssClasses(elementFinder: ElementFinder): Promise { const classAttr: string = await elementFinder.getAttribute('class'); + if (!classAttr) { + return []; + } return classAttr.split(/\s+/); } diff --git a/projects/scion/microfrontend-platform/src/lib/client/connect-options.ts b/projects/scion/microfrontend-platform/src/lib/client/connect-options.ts new file mode 100644 index 00000000..ef6cfeef --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/client/connect-options.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2018-2020 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +/** + * Controls how to connect to the platform host. + * + * @category Platform + */ +export interface ConnectOptions { + /** + * Controls whether to actually connect to the platform host. + * + * Disabling this flag can be useful in tests to not connect to the platform host but still have platform beans available. + * In this mode, messaging is disabled, i.e., sending and receiving messages results in a NOOP. + * + * By default, this flag is set to `true`. + */ + connect?: boolean; + /** + * Specifies the maximum time (in milliseconds) to wait until the message broker is discovered on platform startup. If the broker is not discovered within + * the specified time, platform startup fails with an error. By default, a timeout of 10s is used. + */ + brokerDiscoverTimeout?: number; + /** + * Specifies the maximum time (in milliseconds) that the platform waits to receive dispatch confirmation for messages sent by this application until rejecting + * the publishing Promise. By default, a timeout of 10s is used. + */ + messageDeliveryTimeout?: number; +} diff --git a/projects/scion/microfrontend-platform/src/lib/client/context/context.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/context/context.spec.ts index 497f2c72..8c4b7428 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/context/context.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/context/context.spec.ts @@ -11,6 +11,7 @@ import {MicrofrontendPlatform} from '../../microfrontend-platform'; import {ContextService} from './context-service'; import {Beans} from '@scion/toolkit/bean-manager'; +import {ObserveCaptor} from '@scion/toolkit/testing'; describe('Context', () => { @@ -18,32 +19,24 @@ describe('Context', () => { afterEach(async () => await MicrofrontendPlatform.destroy()); it('should not complete the Observable when looking up context values from inside the host app (no context)', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - let next = undefined; - let error = false; - let complete = false; + const captor = new ObserveCaptor(); + Beans.get(ContextService).observe$('some-context').subscribe(captor); - Beans.get(ContextService).observe$('some-context') - .subscribe(value => next = value, () => error = true, () => complete = true); - - expect(next).toBeNull(); - expect(error).toBeFalse(); - expect(complete).toBeFalse(); + expect(captor.getValues()).toEqual([null]); + expect(captor.hasErrored()).toBeFalse(); + expect(captor.hasCompleted()).toBeFalse(); }); it('should not complete the Observable when looking up the names of context values from inside the host app (no context)', async () => { - await MicrofrontendPlatform.startHost([]); - - let next = undefined; - let error = false; - let complete = false; + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(ContextService).names$() - .subscribe(value => next = value, () => error = true, () => complete = true); + const captor = new ObserveCaptor(); + Beans.get(ContextService).names$().subscribe(captor); - expect(next).toEqual(new Set()); - expect(error).toBeFalse(); - expect(complete).toBeFalse(); + expect(captor.getValues()).toEqual([new Set()]); + expect(captor.hasErrored()).toBeFalse(); + expect(captor.hasCompleted()).toBeFalse(); }); }); diff --git a/projects/scion/microfrontend-platform/src/lib/client/focus/focus-in-event-dispatcher.ts b/projects/scion/microfrontend-platform/src/lib/client/focus/focus-in-event-dispatcher.ts index 2f73a97c..3c38ef94 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/focus/focus-in-event-dispatcher.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/focus/focus-in-event-dispatcher.ts @@ -27,6 +27,8 @@ export class FocusInEventDispatcher implements PreDestroy { private _destroy$ = new Subject(); constructor() { + // IMPORTANT: In Angular applications, the platform should be started outside the Angular zone in order to avoid excessive change detection cycles + // of platform-internal subscriptions to global DOM events. For that reason, we subscribe to `window.focus` events in the dispatcher's constructor. this.makeWindowFocusable(); this.dispatchDocumentFocusInEvent(); this.reportFocusWithinEventToParentOutlet(); @@ -34,10 +36,6 @@ export class FocusInEventDispatcher implements PreDestroy { /** * Installs a listener for `focusin` events. - * - * IMPORTANT: - * Always subscribe to DOM events during event dispatcher construction. Event dispatchers are eagerly created on platform startup. - * Frameworks like Angular usually connect to the platform outside their change detection zone in order to avoid triggering change detection for unrelated DOM events. */ private dispatchDocumentFocusInEvent(): void { fromEvent(window, 'focusin') diff --git a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.spec.ts index f2fe527b..67b1c9f0 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.spec.ts @@ -13,7 +13,6 @@ import {Beans} from '@scion/toolkit/bean-manager'; import {ManifestService} from '../../client/manifest-registry/manifest-service'; import {ObserveCaptor} from '@scion/toolkit/testing'; import {Capability, Intention} from '../../platform.model'; -import {ApplicationConfig} from '../../host/platform-config'; import {ManifestRegistry} from '../../host/manifest-registry/manifest-registry'; const manifestObjectIdsExtractFn = (manifestObjects: Array): string[] => manifestObjects.map(manifestObject => manifestObject.metadata.id); @@ -26,10 +25,7 @@ describe('ManifestService', () => { describe('#lookupCapabilities$', () => { it('should allow looking up own capabilities without declaring an intention (implicit intention)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({host: {symbolicName: 'host-app'}, applications: []}); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -102,11 +98,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app (intention contains the any-more wildcard (**))', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {entity: 'person', '*': '*'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {'entity': 'person', '*': '*'}}, 'host-app'); @@ -182,11 +177,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app (only any-more wildcard (**) intention)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {'*': '*'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {'*': '*'}}, 'host-app'); @@ -262,11 +256,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app (intention contains the asterisk wildcard (*))', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {entity: 'person', id: '*'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -342,11 +335,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app (intention is an exact qualifier)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {entity: 'person', id: 'exact'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: 'exact'}}, 'host-app'); @@ -422,11 +414,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app (intention contains the optional wildcard (?))', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {entity: 'person', id: '?'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '?'}}, 'host-app'); @@ -502,11 +493,10 @@ describe('ManifestService', () => { }); it('should not allow looking up private capabilities of another app', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {'*': '*'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {'*': '*'}}, 'host-app'); @@ -522,11 +512,10 @@ describe('ManifestService', () => { }); it('should allow looking up private capabilities of another app if scope check is disabled', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), scopeCheckDisabled: true}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', scopeCheckDisabled: true}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intention in host-app: {'*': '*'} Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {'*': '*'}}, 'host-app'); @@ -542,11 +531,10 @@ describe('ManifestService', () => { }); it('should not allow looking up public capabilities of another app without matching intention', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register capability Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'app-1'); @@ -559,11 +547,10 @@ describe('ManifestService', () => { }); it('should allow looking up public capabilities of another app without matching intention if intention check is disabled', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionCheckDisabled: true}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionCheckDisabled: true}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register capability const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}, private: false}, 'app-1'); @@ -578,11 +565,10 @@ describe('ManifestService', () => { describe('#lookupIntentions$', () => { it('should allow looking up intentions', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); // Register intentions in host-app const asteriskQualifierHostIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -702,10 +688,10 @@ describe('ManifestService', () => { describe('#removeCapabilities$', () => { it('should allow removing capabilities using an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -741,10 +727,10 @@ describe('ManifestService', () => { }); it('should allow removing capabilities using the asterisk wildcard (*) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -780,10 +766,10 @@ describe('ManifestService', () => { }); it('should interpret the question mark (?) as value (and not as wildcard) when removing capabilities', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -819,10 +805,10 @@ describe('ManifestService', () => { }); it('should allow removing capabilities using an exact qualifier together with the any-more wildcard (**) ', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -858,10 +844,10 @@ describe('ManifestService', () => { }); it('should interpret the question mark (?) as value and not as wildcard when removing capabilities using the any-more wildcard (**)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -897,10 +883,10 @@ describe('ManifestService', () => { }); it('should allow removing capabilities using the asterisk wildcard (*) together with the any-more wildcard (**) ', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -936,10 +922,10 @@ describe('ManifestService', () => { }); it('should allow removing all capabilities using the any-more wildcard (**)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -971,10 +957,10 @@ describe('ManifestService', () => { }); it('should allow removing all capabilities by not specifying a qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capabilities const asteriskQualifierCapability = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1006,11 +992,10 @@ describe('ManifestService', () => { }); it(`should not remove other application's capabilities`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionCheckDisabled: true}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionCheckDisabled: true}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); const capabilityIdHostApp = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}, private: false}, 'host-app'); const capabilityIdApp1 = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}, private: false}, 'app-1'); @@ -1031,10 +1016,10 @@ describe('ManifestService', () => { describe('#removeIntentions$', () => { it('should allow removing intentions using an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1071,10 +1056,10 @@ describe('ManifestService', () => { }); it('should allow removing intentions using the asterisk wildcard (*) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1112,10 +1097,10 @@ describe('ManifestService', () => { }); it('should interpret the question mark (?) as value and not as wildcard when removing intentions', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1152,10 +1137,10 @@ describe('ManifestService', () => { }); it('should allow removing intentions using an exact qualifier together with the any-more wildcard (**) ', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1192,10 +1177,10 @@ describe('ManifestService', () => { }); it('should allow removing intentions using the asterisk wildcard (*) together with the any-more wildcard (**) ', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1232,10 +1217,10 @@ describe('ManifestService', () => { }); it('should interpret the question mark (?) as value (and not as wildcard) when removing intentions using the any-more wildcard (**)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1272,10 +1257,10 @@ describe('ManifestService', () => { }); it('should allow removing all intentions using the any-more wildcard (**)', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1308,10 +1293,10 @@ describe('ManifestService', () => { }); it('should allow removing all intentions by not specifying a qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionRegisterApiDisabled: false}, + applications: [], + }); // Register intentions const asteriskQualifierIntention = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); @@ -1344,11 +1329,10 @@ describe('ManifestService', () => { }); it(`should not remove other application's intentions`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'}), intentionRegisterApiDisabled: false, intentionCheckDisabled: true}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app', intentionCheckDisabled: true, intentionRegisterApiDisabled: false}, + applications: [{symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}], + }); const intentionIdHostApp = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app'); const intentionIdApp1 = Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'app-1'); @@ -1366,4 +1350,5 @@ describe('ManifestService', () => { await expectEmissions(captor).toEqual([[intentionIdApp1]]); }); }); -}); +}) +; diff --git a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.ts b/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.ts index b6aafe4d..78119841 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/manifest-service.ts @@ -17,6 +17,7 @@ import {ManifestRegistryTopics} from '../../host/manifest-registry/ɵmanifest-re import {ManifestObjectFilter} from '../../host/manifest-registry/manifest-object-store'; import {mapToBody} from '../../messaging.model'; import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; +import {BrokerGateway, NullBrokerGateway} from '../messaging/broker-gateway'; /** * Allows looking up capabilities available to the current app and managing the capabilities it provides. @@ -37,13 +38,15 @@ export class ManifestService implements PreDestroy { /** * Promise that resolves when loaded the applications from the host. + * If messaging is disabled, the Promise resolves immediately. * * @internal */ public whenApplicationsLoaded: Promise; - constructor(private _messageClient: MessageClient = Beans.get(MessageClient)) { - this.whenApplicationsLoaded = this.loadApplications(); + constructor() { + const messagingDisabled = Beans.get(BrokerGateway) instanceof NullBrokerGateway; + this.whenApplicationsLoaded = messagingDisabled ? Promise.resolve() : this.requestApplications(); } /** @@ -60,7 +63,7 @@ export class ManifestService implements PreDestroy { * @deprecated since version 1.0.0-beta.8. Use {@link applications} instead. */ public lookupApplications$(): Observable { - return this._messageClient.observe$(PlatformTopics.Applications) + return Beans.get(MessageClient).observe$(PlatformTopics.Applications) .pipe( take(1), mapToBody(), @@ -85,7 +88,7 @@ export class ManifestService implements PreDestroy { * It never completes and emits continuously when satisfying capabilities are registered or unregistered. */ public lookupCapabilities$(filter?: ManifestObjectFilter): Observable { - return this._messageClient.request$(ManifestRegistryTopics.LookupCapabilities, filter) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.LookupCapabilities, filter) .pipe(mapToBody()); } @@ -102,7 +105,7 @@ export class ManifestService implements PreDestroy { * It never completes and emits continuously when satisfying intentions are registered or unregistered. */ public lookupIntentions$(filter?: ManifestObjectFilter): Observable { - return this._messageClient.request$(ManifestRegistryTopics.LookupIntentions, filter) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.LookupIntentions, filter) .pipe(mapToBody()); } @@ -113,7 +116,7 @@ export class ManifestService implements PreDestroy { * or that rejects if the registration failed. */ public registerCapability(capability: T): Promise { - return this._messageClient.request$(ManifestRegistryTopics.RegisterCapability, capability) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.RegisterCapability, capability) .pipe(mapToBody()) .toPromise(); } @@ -134,7 +137,7 @@ export class ManifestService implements PreDestroy { * or that rejects if the unregistration failed. */ public unregisterCapabilities(filter?: ManifestObjectFilter): Promise { - return this._messageClient.request$(ManifestRegistryTopics.UnregisterCapabilities, filter) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.UnregisterCapabilities, filter) .pipe(mergeMapTo(EMPTY)) .toPromise() .then(() => Promise.resolve()); // resolve to `void` @@ -149,7 +152,7 @@ export class ManifestService implements PreDestroy { * or that rejects if the registration failed. */ public registerIntention(intention: Intention): Promise { - return this._messageClient.request$(ManifestRegistryTopics.RegisterIntention, intention) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.RegisterIntention, intention) .pipe(mapToBody()) .toPromise(); } @@ -170,14 +173,14 @@ export class ManifestService implements PreDestroy { * or that rejects if the unregistration failed. */ public unregisterIntentions(filter?: ManifestObjectFilter): Promise { - return this._messageClient.request$(ManifestRegistryTopics.UnregisterIntentions, filter) + return Beans.get(MessageClient).request$(ManifestRegistryTopics.UnregisterIntentions, filter) .pipe(mergeMapTo(EMPTY)) .toPromise() .then(() => Promise.resolve()); // resolve to `void` } - private async loadApplications(): Promise { - this._applications = await this._messageClient.observe$(PlatformTopics.Applications) + private async requestApplications(): Promise { + this._applications = await Beans.get(MessageClient).observe$(PlatformTopics.Applications) .pipe(mapToBody(), take(1), takeUntil(this._destroy$)) .toPromise() .then(applications => applications || []); diff --git a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/platform-manifest-service.ts b/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/platform-manifest-service.ts deleted file mode 100644 index 5cb80dc4..00000000 --- a/projects/scion/microfrontend-platform/src/lib/client/manifest-registry/platform-manifest-service.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -import {ManifestService} from './manifest-service'; -import {PlatformMessageClient} from '../../host/platform-message-client'; -import {Beans} from '@scion/toolkit/bean-manager'; - -/** - * Manifest service used by the platform to interact with the manifest registry. - * - * The interaction is on behalf of the platform app {@link PLATFORM_SYMBOLIC_NAME}. - * - * @ignore - */ -export class PlatformManifestService extends ManifestService { - - constructor() { - super(Beans.get(PlatformMessageClient)); - } -} diff --git a/projects/scion/microfrontend-platform/src/lib/client/messaging/broker-gateway.ts b/projects/scion/microfrontend-platform/src/lib/client/messaging/broker-gateway.ts index 23d9cb14..e51a2852 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/messaging/broker-gateway.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/messaging/broker-gateway.ts @@ -17,7 +17,7 @@ import {GatewayInfoResponse, getGatewayJavaScript} from './broker-gateway-script import {Logger, NULL_LOGGER} from '../../logger'; import {Dictionaries} from '@scion/toolkit/util'; import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; -import {IS_PLATFORM_HOST} from '../../platform.model'; +import {APP_IDENTITY, IS_PLATFORM_HOST} from '../../platform.model'; import {Runlevel} from '../../platform-state'; /** @@ -123,13 +123,16 @@ export class NullBrokerGateway implements BrokerGateway { export class ɵBrokerGateway implements BrokerGateway, PreDestroy { private _destroy$ = new Subject(); + private _appSymbolicName: string; private _whenDestroy = this._destroy$.pipe(first()).toPromise(); private _message$: Observable; private _whenGatewayInfo: Promise; - constructor(private _clientAppName: string, private _config: {discoveryTimeout: number; deliveryTimeout: number}) { + constructor(private _config: {messageDeliveryTimeout: number; brokerDiscoveryTimeout: number}) { + this._appSymbolicName = Beans.get(APP_IDENTITY); + // Get the JavaScript to discover the message broker and dispatch messages. - const gatewayJavaScript = getGatewayJavaScript({clientAppName: this._clientAppName, clientOrigin: window.origin, discoverTimeout: this._config.discoveryTimeout}); + const gatewayJavaScript = getGatewayJavaScript({clientAppName: this._appSymbolicName, clientOrigin: window.origin, discoverTimeout: this._config.brokerDiscoveryTimeout}); const isPlatformHost = Beans.get(IS_PLATFORM_HOST); @@ -139,7 +142,7 @@ export class ɵBrokerGateway implements BrokerGateway, PreDestroy { // we initiate the connect request to the broker only once entering runlevel 1. this._whenGatewayInfo = (isPlatformHost ? Beans.whenRunlevel(Runlevel.One) : Promise.resolve()) .then(() => this.mountIframeAndLoadScript(gatewayJavaScript)) // Create a hidden iframe and load the gateway script. - .then(gatewayWindow => this.requestGatewayInfo(gatewayWindow, {brokerDiscoveryTimeout: this._config.discoveryTimeout})) + .then(gatewayWindow => this.requestGatewayInfo(gatewayWindow, {brokerDiscoveryTimeout: this._config.brokerDiscoveryTimeout})) .catch(error => { Beans.get(Logger, {orElseGet: NULL_LOGGER}).error(error); // Fall back using NULL_LOGGER when the platform is shutting down. throw error; @@ -182,7 +185,7 @@ export class ɵBrokerGateway implements BrokerGateway, PreDestroy { message: message, }; envelope.message.headers.set(MessageHeaders.MessageId, messageId); - addSenderHeadersToEnvelope(envelope, {clientAppName: this._clientAppName, clientId: gateway.clientId}); + addSenderHeadersToEnvelope(envelope, {clientAppName: this._appSymbolicName, clientId: gateway.clientId}); // Create Promise waiting for the broker to accept and dispatch the message. const postError$ = new Subject(); @@ -190,7 +193,7 @@ export class ɵBrokerGateway implements BrokerGateway, PreDestroy { .pipe( filterByTopic(messageId), first(), - timeoutWith(new Date(Date.now() + this._config.deliveryTimeout), throwError(`[MessageDispatchError] Broker did not report message delivery state within the ${this._config.deliveryTimeout}ms timeout. [envelope=${stringifyEnvelope(envelope)}]`)), + timeoutWith(new Date(Date.now() + this._config.messageDeliveryTimeout), throwError(`[MessageDispatchError] Broker did not report message delivery state within the ${this._config.messageDeliveryTimeout}ms timeout. [envelope=${stringifyEnvelope(envelope)}]`)), mergeMap(statusMessage => statusMessage.body!.ok ? EMPTY : throwError(statusMessage.body!.details)), takeUntil(this._destroy$), ) @@ -270,7 +273,7 @@ export class ɵBrokerGateway implements BrokerGateway, PreDestroy { * @return A Promise that resolves to the content window of the iframe. */ private mountIframeAndLoadScript(javaScript: string): Promise { - const html = `Message Broker Gateway for '${this._clientAppName}'`; + const html = `Message Broker Gateway for '${this._appSymbolicName}'`; const iframeUrl = URL.createObjectURL(new Blob([html], {type: 'text/html'})); const iframe = document.body.appendChild(document.createElement('iframe')); iframe.setAttribute('src', iframeUrl); @@ -335,7 +338,7 @@ export class ɵBrokerGateway implements BrokerGateway, PreDestroy { ) .toPromise(); - addSenderHeadersToEnvelope(request, {clientAppName: this._clientAppName}); + addSenderHeadersToEnvelope(request, {clientAppName: this._appSymbolicName}); gatewayWindow.postMessage(request, gatewayWindow.origin); return whenReply.then(neverResolveIfUndefined); } diff --git a/projects/scion/microfrontend-platform/src/lib/client/messaging/message-client.ts b/projects/scion/microfrontend-platform/src/lib/client/messaging/message-client.ts index 61f40549..8219fe39 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/messaging/message-client.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/messaging/message-client.ts @@ -10,7 +10,7 @@ import {MonoTypeOperatorFunction, Observable, Subscription} from 'rxjs'; import {TopicMessage} from '../../messaging.model'; import {first, takeUntil} from 'rxjs/operators'; -import {AbstractType, Beans, Type} from '@scion/toolkit/bean-manager'; +import {Beans} from '@scion/toolkit/bean-manager'; /** * Message client for sending and receiving messages between microfrontends across origins. @@ -161,8 +161,8 @@ export abstract class MessageClient { * * @category Messaging */ -export function takeUntilUnsubscribe(topic: string, /* @internal */ messageClientType?: Type | AbstractType): MonoTypeOperatorFunction { - return takeUntil(Beans.get(messageClientType || MessageClient).subscriberCount$(topic).pipe(first(count => count === 0))); +export function takeUntilUnsubscribe(topic: string): MonoTypeOperatorFunction { + return takeUntil(Beans.get(MessageClient).subscriberCount$(topic).pipe(first(count => count === 0))); } /** diff --git a/projects/scion/microfrontend-platform/src/lib/client/messaging/message-handler.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/messaging/message-handler.spec.ts index f5b86933..f5477a94 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/messaging/message-handler.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/messaging/message-handler.spec.ts @@ -8,10 +8,9 @@ * SPDX-License-Identifier: EPL-2.0 */ import {MessageClient} from '../../client/messaging/message-client'; -import {expectPromise, serveManifest, waitFor, waitForCondition} from '../../spec.util.spec'; +import {expectPromise, waitFor, waitForCondition} from '../../spec.util.spec'; import {MicrofrontendPlatform} from '../../microfrontend-platform'; import {Beans} from '@scion/toolkit/bean-manager'; -import {ApplicationConfig} from '../../host/platform-config'; import {IntentMessage, TopicMessage} from '../../messaging.model'; import {AsyncSubject, concat, Observable, of, ReplaySubject, Subject, throwError} from 'rxjs'; import {finalize} from 'rxjs/operators'; @@ -28,9 +27,7 @@ describe('Message Handler', () => { describe('pub/sub', () => { it('should receive messages published to a topic', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); Beans.get(MessageClient).onMessage('topic', message => { @@ -46,9 +43,7 @@ describe('Message Handler', () => { }); it('should not unregister the callback on error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); Beans.get(MessageClient).onMessage('topic', message => { @@ -65,9 +60,7 @@ describe('Message Handler', () => { }); it('should not unregister the callback on async error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); Beans.get(MessageClient).onMessage('topic', async message => { @@ -84,9 +77,7 @@ describe('Message Handler', () => { }); it('should ignore values returned by the callback', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); Beans.get(MessageClient).onMessage('topic', message => { @@ -103,9 +94,7 @@ describe('Message Handler', () => { }); it('should ignore async values returned by the callback', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); Beans.get(MessageClient).onMessage('topic', message => { @@ -122,9 +111,7 @@ describe('Message Handler', () => { }); it('should unregister the handler when cancelling its subscription', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collector = new Array(); const subscription = Beans.get(MessageClient).onMessage('topic', message => { @@ -146,9 +133,7 @@ describe('Message Handler', () => { describe('request/response', () => { it('should reply with a single response and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { return message.body.toUpperCase(); @@ -163,9 +148,7 @@ describe('Message Handler', () => { }); it('should reply with a single response (Promise) and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { return Promise.resolve(message.body.toUpperCase()); @@ -180,9 +163,7 @@ describe('Message Handler', () => { }); it('should reply with a single response (Observable) and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { return of(message.body.toUpperCase()); @@ -197,9 +178,7 @@ describe('Message Handler', () => { }); it('should reply with multiple responses and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { const body = message.body.toUpperCase(); @@ -215,9 +194,7 @@ describe('Message Handler', () => { }); it('should reply with multiple responses without completing the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { const body = message.body.toUpperCase(); @@ -240,9 +217,7 @@ describe('Message Handler', () => { }); it('should immediately complete the requestor\'s Observable when not returning a value', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { // not returning a value @@ -257,9 +232,7 @@ describe('Message Handler', () => { }); it('should immediately complete the requestor\'s Observable when returning `undefined`', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { return undefined; @@ -274,9 +247,7 @@ describe('Message Handler', () => { }); it('should treat `null` as valid reply', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { return null; @@ -291,9 +262,7 @@ describe('Message Handler', () => { }); it('should ignore `undefined` values, but not `null` values', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { const body = message.body.toUpperCase(); @@ -309,9 +278,7 @@ describe('Message Handler', () => { }); it('should error the requestor\'s Observable when throwing an error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { throw Error('some error'); @@ -328,9 +295,7 @@ describe('Message Handler', () => { }); it('should error the requestor\'s Observable when returning a Promise that rejects', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { return Promise.reject('some error'); @@ -347,9 +312,7 @@ describe('Message Handler', () => { }); it('should error the requestor\'s Observable when returning an Observable that errors', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', () => { return throwError('some error'); @@ -366,9 +329,7 @@ describe('Message Handler', () => { }); it('should reply values until encountering an error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { return concat( @@ -388,9 +349,7 @@ describe('Message Handler', () => { }); it('should not unregister the handler if the replier Observable errors', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); Beans.get(MessageClient).onMessage('topic', message => { return concat( @@ -419,9 +378,7 @@ describe('Message Handler', () => { }); it('should not unregister the handler on error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collected = []; Beans.get(MessageClient).onMessage('topic', message => { @@ -449,9 +406,7 @@ describe('Message Handler', () => { }); it('should not unregister the handler on async error', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const collected = []; Beans.get(MessageClient).onMessage('topic', message => { @@ -479,9 +434,7 @@ describe('Message Handler', () => { }); it('should unsubscribe from the replier Observable when the requestor unsubscribes', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const replierConstruct$ = new Subject(); const whenReplierConstruct = replierConstruct$.toPromise(); @@ -508,9 +461,7 @@ describe('Message Handler', () => { }); it('should unsubscribe the replier\'s and requestor\'s Observable when unregistering the handler', async () => { - const manifestUrl = serveManifest({name: 'Host App'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({applications: []}); const replierConstruct$ = new AsyncSubject(); const replierTeardown$ = new AsyncSubject(); @@ -549,9 +500,15 @@ describe('Intent Handler', () => { describe('pub/sub', () => { it('should receive intents', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -567,9 +524,15 @@ describe('Intent Handler', () => { }); it('should not unregister the callback on error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -586,9 +549,15 @@ describe('Intent Handler', () => { }); it('should not unregister the callback on async error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); Beans.get(IntentClient).onIntent({type: 'capability'}, async intentMessage => { @@ -605,9 +574,15 @@ describe('Intent Handler', () => { }); it('should ignore values returned by the callback', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -624,9 +599,15 @@ describe('Intent Handler', () => { }); it('should ignore async values returned by the callback', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -643,9 +624,15 @@ describe('Intent Handler', () => { }); it('should unregister the handler when cancelling its subscription', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collector = new Array(); const subscription = Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -667,9 +654,15 @@ describe('Intent Handler', () => { describe('request/response', () => { it('should reply with a single response and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { return intentMessage.body.toUpperCase(); @@ -684,9 +677,15 @@ describe('Intent Handler', () => { }); it('should reply with a single response (Promise) and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { return Promise.resolve(intentMessage.body.toUpperCase()); @@ -701,9 +700,15 @@ describe('Intent Handler', () => { }); it('should reply with a single response (Observable) and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { return of(intentMessage.body.toUpperCase()); @@ -718,9 +723,15 @@ describe('Intent Handler', () => { }); it('should reply with multiple responses and then complete the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { const body = intentMessage.body.toUpperCase(); @@ -736,9 +747,15 @@ describe('Intent Handler', () => { }); it('should reply with multiple responses without completing the requestor\'s Observable', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { const body = intentMessage.body.toUpperCase(); @@ -761,9 +778,15 @@ describe('Intent Handler', () => { }); it('should immediately complete the requestor\'s Observable when not returning a value', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { // not returning a value @@ -778,9 +801,15 @@ describe('Intent Handler', () => { }); it('should immediately complete the requestor\'s Observable when returning `undefined`', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { return undefined; @@ -795,9 +824,15 @@ describe('Intent Handler', () => { }); it('should treat `null` as valid reply', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { return null; @@ -812,9 +847,15 @@ describe('Intent Handler', () => { }); it('should ignore `undefined` values, but not `null` values', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { const body = intentMessage.body.toUpperCase(); @@ -830,9 +871,15 @@ describe('Intent Handler', () => { }); it('should error the requestor\'s Observable when throwing an error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { throw Error('some error'); @@ -849,9 +896,15 @@ describe('Intent Handler', () => { }); it('should error the requestor\'s Observable when returning a Promise that rejects', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { return Promise.reject('some error'); @@ -868,9 +921,15 @@ describe('Intent Handler', () => { }); it('should error the requestor\'s Observable when returning an Observable that errors', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, () => { return throwError('some error'); @@ -887,9 +946,15 @@ describe('Intent Handler', () => { }); it('should reply values until encountering an error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { return concat( @@ -909,9 +974,15 @@ describe('Intent Handler', () => { }); it('should not unregister the handler if the replier Observable errors', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { return concat( @@ -940,9 +1011,15 @@ describe('Intent Handler', () => { }); it('should not unregister the handler on error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collected = []; Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -970,9 +1047,15 @@ describe('Intent Handler', () => { }); it('should not unregister the handler on async error', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const collected = []; Beans.get(IntentClient).onIntent({type: 'capability'}, intentMessage => { @@ -1000,9 +1083,15 @@ describe('Intent Handler', () => { }); it('should unsubscribe from the replier Observable when the requestor unsubscribes', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const replierConstruct$ = new Subject(); const whenReplierConstruct = replierConstruct$.toPromise(); @@ -1029,9 +1118,15 @@ describe('Intent Handler', () => { }); it('should unsubscribe the replier\'s and requestor\'s Observable when unregistering the handler', async () => { - const manifestUrl = serveManifest({name: 'Host App', capabilities: [{type: 'capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host App', + capabilities: [{type: 'capability'}], + }, + }, + applications: [], + }); const replierConstruct$ = new AsyncSubject(); const replierTeardown$ = new AsyncSubject(); diff --git a/projects/scion/microfrontend-platform/src/lib/client/messaging/messaging.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/messaging/messaging.spec.ts index c5208650..e669df97 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/messaging/messaging.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/messaging/messaging.spec.ts @@ -7,21 +7,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {PlatformMessageClient} from '../../host/platform-message-client'; import {first, publishReplay, timeoutWith} from 'rxjs/operators'; import {ConnectableObservable, noop, Observable, Subject, throwError} from 'rxjs'; import {IntentMessage, MessageHeaders, ResponseStatusCodes, TopicMessage} from '../../messaging.model'; import {MessageClient, takeUntilUnsubscribe} from './message-client'; import {IntentClient} from './intent-client'; -import {ManifestRegistry} from '../../host/manifest-registry/manifest-registry'; -import {ApplicationConfig} from '../../host/platform-config'; -import {PLATFORM_SYMBOLIC_NAME} from '../../host/platform.constants'; import {expectEmissions, expectPromise, getLoggerSpy, installLoggerSpies, readConsoleLog, resetLoggerSpy, serveManifest, waitForCondition} from '../../spec.util.spec'; import {MicrofrontendPlatform} from '../../microfrontend-platform'; import {Defined, Objects} from '@scion/toolkit/util'; import {ClientRegistry} from '../../host/message-broker/client.registry'; import {MessageEnvelope} from '../../ɵmessaging.model'; -import {PlatformIntentClient} from '../../host/platform-intent-client'; import {Beans} from '@scion/toolkit/bean-manager'; import {ManifestService} from '../manifest-registry/manifest-service'; import {ObserveCaptor} from '@scion/toolkit/testing'; @@ -35,7 +30,7 @@ const capabilityIdExtractFn = (msg: IntentMessage): string => msg.capabili /** * Tests most important and fundamental features of the messaging facility with a single client, the host-app, only. * - * More advanced and deeper testing with having multiple, cross-origin clients connected, is done end-to-end with Protractor against the testing app. + * More sophisticated and real-world testing with multiple, cross-origin clients is done end-to-end with Protractor against the testing app. * * See `messaging.e2e-spec.ts` for end-to-end tests. */ @@ -48,22 +43,30 @@ describe('Messaging', () => { afterEach(async () => await MicrofrontendPlatform.destroy()); it('should allow publishing messages to a topic', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const messageCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(messageCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(messageCaptor); - await Beans.get(PlatformMessageClient).publish('some-topic', 'A'); - await Beans.get(PlatformMessageClient).publish('some-topic', 'B'); - await Beans.get(PlatformMessageClient).publish('some-topic', 'C'); + await Beans.get(MessageClient).publish('some-topic', 'A'); + await Beans.get(MessageClient).publish('some-topic', 'B'); + await Beans.get(MessageClient).publish('some-topic', 'C'); await expectEmissions(messageCaptor).toEqual(['A', 'B', 'C']); }); it('should allow issuing an intent', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + {type: 'some-capability'}, + ], + }, + }, + applications: [], + }); const intentCaptor = new ObserveCaptor(bodyExtractFn); Beans.get(IntentClient).observe$().subscribe(intentCaptor); @@ -74,36 +77,51 @@ describe('Messaging', () => { }); it('should allow issuing an intent for which the app has not declared a respective intention, but only if \'intention check\' is disabled for that app', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const clientManifestUrl = serveManifest({name: 'Client Application', capabilities: [{type: 'some-type', private: false}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl, intentionCheckDisabled: true}, {symbolicName: 'client-app', manifestUrl: clientManifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: {intentionCheckDisabled: true}, + applications: [ + { + symbolicName: 'client-app', + manifestUrl: serveManifest({ + name: 'Client Application', + capabilities: [{type: 'some-type', private: false}], + }), + }, + ], + }); await expectPromise(Beans.get(IntentClient).publish({type: 'some-type'})).toResolve(); }); it('should not allow issuing an intent for which the app has not declared a respective intention, if \'intention check\' is enabled or not specified for that app', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const clientManifestUrl = serveManifest({name: 'Client Application', capabilities: [{type: 'some-type', private: false}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}, {symbolicName: 'client-app', manifestUrl: clientManifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + applications: [ + { + symbolicName: 'client-app', + manifestUrl: serveManifest({ + name: 'Client Application', + capabilities: [{type: 'some-type', private: false}], + }), + }, + ], + }); await expectPromise(Beans.get(IntentClient).publish({type: 'some-type'})).toReject(/NotQualifiedError/); }); it('should dispatch a message to subscribers with a wildcard subscription', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const messageCaptor = new ObserveCaptor(); // Subscribe to 'myhome/:room/temperature' - Beans.get(PlatformMessageClient).observe$('myhome/:room/temperature').subscribe(messageCaptor); + Beans.get(MessageClient).observe$('myhome/:room/temperature').subscribe(messageCaptor); // Publish messages - await Beans.get(PlatformMessageClient).publish('myhome/livingroom/temperature', '25°C'); - await Beans.get(PlatformMessageClient).publish('myhome/livingroom/temperature', '26°C'); - await Beans.get(PlatformMessageClient).publish('myhome/kitchen/temperature', '22°C'); - await Beans.get(PlatformMessageClient).publish('myhome/kitchen/humidity', '15%'); + await Beans.get(MessageClient).publish('myhome/livingroom/temperature', '25°C'); + await Beans.get(MessageClient).publish('myhome/livingroom/temperature', '26°C'); + await Beans.get(MessageClient).publish('myhome/kitchen/temperature', '22°C'); + await Beans.get(MessageClient).publish('myhome/kitchen/humidity', '15%'); await messageCaptor.waitUntilEmitCount(3); @@ -130,20 +148,26 @@ describe('Messaging', () => { }); it('should allow passing headers when publishing a message', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const headerCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headerCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headerCaptor); - await Beans.get(PlatformMessageClient).publish('some-topic', undefined, {headers: new Map().set('header1', 'value').set('header2', 42)}); + await Beans.get(MessageClient).publish('some-topic', undefined, {headers: new Map().set('header1', 'value').set('header2', 42)}); await headerCaptor.waitUntilEmitCount(1); await expect(headerCaptor.getLastValue()).toEqual(jasmine.mapContaining(new Map().set('header1', 'value').set('header2', 42))); }); it('should allow passing headers when issuing an intent', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); const headerCaptor = new ObserveCaptor(headersExtractFn); Beans.get(IntentClient).observe$().subscribe(headerCaptor); @@ -154,34 +178,40 @@ describe('Messaging', () => { }); it('should return an empty headers dictionary if no headers are set', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const headerCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headerCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headerCaptor); - await Beans.get(PlatformMessageClient).publish('some-topic', 'payload'); + await Beans.get(MessageClient).publish('some-topic', 'payload'); await headerCaptor.waitUntilEmitCount(1); await expect(headerCaptor.getLastValue()).toEqual(jasmine.mapContaining(new Map())); }); it('should allow passing headers when sending a request', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, undefined, {headers: new Map().set('reply-header', msg.headers.get('request-header').toUpperCase())}); + Beans.get(MessageClient).publish(replyTo, undefined, {headers: new Map().set('reply-header', msg.headers.get('request-header').toUpperCase())}); }); const replyHeaderCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', undefined, {headers: new Map().set('request-header', 'ping')}).subscribe(replyHeaderCaptor); + Beans.get(MessageClient).request$('some-topic', undefined, {headers: new Map().set('request-header', 'ping')}).subscribe(replyHeaderCaptor); await replyHeaderCaptor.waitUntilEmitCount(1); await expect(replyHeaderCaptor.getLastValue()).toEqual(jasmine.mapContaining(new Map().set('reply-header', 'PING'))); }); it('should allow passing headers when sending an intent request', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -194,118 +224,95 @@ describe('Messaging', () => { await expect(replyHeaderCaptor.getLastValue()).toEqual(jasmine.mapContaining(new Map().set('reply-header', 'PING'))); }); - it('should transport a topic message to both, the platform client and the host client, respectively', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); - - // Observe messages using the {PlatformMessageClient} - const platformMessageCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(platformMessageCaptor); - - // Observe messages using the {MessageClient} - const clientMessageCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(MessageClient).observe$('some-topic').subscribe(clientMessageCaptor); - - // Wait until subscribed - await waitUntilSubscriberCount('some-topic', 2); - - await Beans.get(PlatformMessageClient).publish('some-topic', 'A'); - await Beans.get(MessageClient).publish('some-topic', 'B'); - - await expectEmissions(platformMessageCaptor).toEqual(['A', 'B']); - await expectEmissions(clientMessageCaptor).toEqual(['A', 'B']); - }); - it('should allow receiving a reply for a request (by not replying with a status code)', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING']); expect(replyCaptor.hasCompleted()).toBeFalse(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should allow receiving a reply for a request (by replying with the status code 200)', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING']); expect(replyCaptor.hasCompleted()).toBeFalse(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should allow receiving multiple replies for a request', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING', 'PING', 'PING']); expect(replyCaptor.hasCompleted()).toBeFalse(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should complete the request when replying with the status code 250 (with the first reply)', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING']); expect(replyCaptor.hasCompleted()).toBeTrue(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should complete the request when replying with the status code 250 (after multiple replies)', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase()); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING', 'PING', 'PING']); expect(replyCaptor.hasCompleted()).toBeTrue(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should error the request when replying with the status code 500', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(msg => { + Beans.get(MessageClient).observe$('some-topic').subscribe(msg => { const replyTo = msg.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + Beans.get(MessageClient).publish(replyTo, msg.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); }); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await replyCaptor.waitUntilCompletedOrErrored(); expect(replyCaptor.getValues()).toEqual([]); expect(replyCaptor.hasCompleted()).toBeFalse(); @@ -314,9 +321,15 @@ describe('Messaging', () => { }); it('should allow receiving a reply for an intent request (by not replying with a status code)', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -331,9 +344,15 @@ describe('Messaging', () => { }); it('should allow receiving a reply for an intent request (by replying with the status code 200)', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -348,9 +367,15 @@ describe('Messaging', () => { }); it('should allow receiving multiple replies for an intent request', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -367,9 +392,15 @@ describe('Messaging', () => { }); it('should complete the intent request when replying with the status code 250 (with the first reply)', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -384,14 +415,20 @@ describe('Messaging', () => { }); it('should complete the intent request when replying with the status code 250 (after multiple replies)', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); - Beans.get(PlatformMessageClient).publish(replyTo, intent.body.toUpperCase()); - Beans.get(PlatformMessageClient).publish(replyTo, intent.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, intent.body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, intent.body.toUpperCase()); Beans.get(MessageClient).publish(replyTo, intent.body.toUpperCase(), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); }); @@ -403,9 +440,15 @@ describe('Messaging', () => { }); it('should error the intent request when replying with the status code 500', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); Beans.get(IntentClient).observe$().subscribe(intent => { const replyTo = intent.headers.get(MessageHeaders.ReplyTo); @@ -422,10 +465,23 @@ describe('Messaging', () => { }); it('should reject a \'request-response\' intent if no replier is found', async () => { - const manifestUrl = serveManifest({name: 'Host Application', intentions: [{type: 'some-type'}]}); - const clientManifestUrl = serveManifest({name: 'Client Application', capabilities: [{type: 'some-type', private: false}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}, {symbolicName: 'client-app', manifestUrl: clientManifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + intentions: [{type: 'some-type'}], + }, + }, + applications: [ + { + symbolicName: 'client-app', + manifestUrl: serveManifest({ + name: 'Client Application', + capabilities: [{type: 'some-type', private: false}], + }), + }, + ], + }); const replyCaptor = new ObserveCaptor(); Beans.get(IntentClient).request$({type: 'some-type'}, 'ping').subscribe(replyCaptor); @@ -434,10 +490,10 @@ describe('Messaging', () => { }); it('should reject a \'request-response\' topic message if no replier is found', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const replyCaptor = new ObserveCaptor(); - Beans.get(PlatformMessageClient).request$('some-topic').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic').subscribe(replyCaptor); await replyCaptor.waitUntilCompletedOrErrored(); expect(replyCaptor.getValues()).toEqual([]); expect(replyCaptor.hasCompleted()).toBeFalse(); @@ -452,7 +508,7 @@ describe('Messaging', () => { if (message.intent.type === 'some-capability') { const replyTo = message.headers.get(MessageHeaders.ReplyTo); const body = message.body; - Beans.get(PlatformMessageClient).publish(replyTo, body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, body.toUpperCase()); } else { next.handle(message); @@ -460,10 +516,23 @@ describe('Messaging', () => { } }; Beans.register(IntentInterceptor, {useValue: interceptor, multi: true}); - const manifestUrl = serveManifest({name: 'Host Application', intentions: [{type: 'some-capability'}]}); - const clientManifestUrl = serveManifest({name: 'Client Application', capabilities: [{type: 'some-capability', private: false}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}, {symbolicName: 'client-app', manifestUrl: clientManifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + intentions: [{type: 'some-capability'}], + }, + }, + applications: [ + { + symbolicName: 'client-app', + manifestUrl: serveManifest({ + name: 'Client Application', + capabilities: [{type: 'some-capability', private: false}], + }), + }, + ], + }); const replyCaptor = new ObserveCaptor(bodyExtractFn); Beans.get(IntentClient).request$({type: 'some-capability'}, 'ping').subscribe(replyCaptor); @@ -479,7 +548,7 @@ describe('Messaging', () => { if (message.topic === 'some-topic') { const replyTo = message.headers.get(MessageHeaders.ReplyTo); const body = message.body; - Beans.get(PlatformMessageClient).publish(replyTo, body.toUpperCase()); + Beans.get(MessageClient).publish(replyTo, body.toUpperCase()); } else { next.handle(message); @@ -487,19 +556,25 @@ describe('Messaging', () => { } }; Beans.register(MessageInterceptor, {useValue: interceptor, multi: true}); - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const replyCaptor = new ObserveCaptor(bodyExtractFn); - Beans.get(PlatformMessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); + Beans.get(MessageClient).request$('some-topic', 'ping').subscribe(replyCaptor); await expectEmissions(replyCaptor).toEqual(['PING']); expect(replyCaptor.hasCompleted()).toBeFalse(); expect(replyCaptor.hasErrored()).toBeFalse(); }); it('should reject an intent if no application provides a satisfying capability', async () => { - const manifestUrl = serveManifest({name: 'Host Application', intentions: [{type: 'some-type'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + intentions: [{type: 'some-type'}], + }, + }, + applications: [], + }); const replyCaptor = new ObserveCaptor(); Beans.get(IntentClient).request$({type: 'some-type'}, 'ping').subscribe(replyCaptor); @@ -508,7 +583,7 @@ describe('Messaging', () => { }); it('should reject a client connect attempt if the app is not registered', async () => { - await MicrofrontendPlatform.startHost([]); // no app is registered + await MicrofrontendPlatform.startHost({applications: []}); // no app is registered const badClient = mountBadClientAndConnect({symbolicName: 'bad-client'}); try { @@ -523,9 +598,17 @@ describe('Messaging', () => { }); it('should reject a client connect attempt if the client\'s origin is different to the registered app origin', async () => { - const manifestUrl = serveManifest({name: 'Client', baseUrl: 'http://app-origin'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'client', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps); + await MicrofrontendPlatform.startHost({ + applications: [ + { + symbolicName: 'client', + manifestUrl: serveManifest({ + name: 'Client', + baseUrl: 'http://app-origin', + }), + }, + ], + }); const badClient = mountBadClientAndConnect({symbolicName: 'client'}); // bad client connects under karma test runner origin (window.origin) try { @@ -540,15 +623,14 @@ describe('Messaging', () => { it('should reject startup promise if the message broker cannot be discovered', async () => { const loggerSpy = getLoggerSpy('error'); - const startup = MicrofrontendPlatform.connectToHost({symbolicName: 'client-app', messaging: {brokerDiscoverTimeout: 250}}); - await expectPromise(startup).toReject(/PlatformStartupError/); + const startup = MicrofrontendPlatform.connectToHost('client-app', {brokerDiscoverTimeout: 250}); + await expectPromise(startup).toReject(/MicrofrontendPlatformStartupError/); await expect(loggerSpy).toHaveBeenCalledWith('[BrokerDiscoverTimeoutError] Message broker not discovered within the 250ms timeout. Messages cannot be published or received.'); }); it('should not error with `BrokerDiscoverTimeoutError` when starting the platform host and initializers in runlevel 0 take a long time to complete, e.g., when fetching manifests', async () => { const loggerSpy = getLoggerSpy('error'); - const brokerDiscoverTimeout = 250; const initializerDuration = 1000; // Register a long running initializer in runlevel 0 @@ -561,9 +643,7 @@ describe('Messaging', () => { runlevel: 0, }); - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - const startup = MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout}}); + const startup = MicrofrontendPlatform.startHost({applications: []}); await expectPromise(startup).toResolve(); expect(initializerCompleted).toBeTrue(); @@ -572,7 +652,6 @@ describe('Messaging', () => { it('should not error with `BrokerDiscoverTimeoutError` when publishing a message in runlevel 0 and runlevel 0 takes a long time to complete (messaging only enabled in runlevel 2)', async () => { const loggerSpy = getLoggerSpy('error'); - const brokerDiscoverTimeout = 250; const initializerDuration = 1000; // Publish a message in runlevel 0 @@ -593,9 +672,7 @@ describe('Messaging', () => { }); // Start the host - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - const startup = MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout}}); + const startup = MicrofrontendPlatform.startHost({applications: []}); // Expect the startup not to error await expectPromise(startup).toResolve(); @@ -608,59 +685,8 @@ describe('Messaging', () => { expect(messageCaptor.getLastValue()).toEqual('payload'); }); - describe('Separate registries for the platform and the host client app', () => { - - it('should dispatch an intent only to the platform intent client', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); - - // Register a platform capability. Intents should not be received by the host-app intent client. - await Beans.get(ManifestRegistry).registerCapability({type: 'some-capability'}, PLATFORM_SYMBOLIC_NAME); - const platformClientIntentCaptor = new ObserveCaptor(); - Beans.get(PlatformIntentClient).observe$().subscribe(platformClientIntentCaptor); - const hostClientIntentCaptor = new ObserveCaptor(); - Beans.get(IntentClient).observe$().subscribe(hostClientIntentCaptor); - - // Issue the intent via platform intent client. - await Beans.get(PlatformIntentClient).publish({type: 'some-capability'}); - - // Verify host-app intent client not receiving the intent. - expect(platformClientIntentCaptor.getLastValue()).toBeDefined(); - expect(hostClientIntentCaptor.getLastValue()).toBeUndefined(); - - // Verify host-app intent client not allowed to issue the intent. - await expectPromise(Beans.get(IntentClient).publish({type: 'some-capability'})).toReject(/NotQualifiedError/); - }); - - it('should dispatch an intent only to the host-app intent client', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); - - // Register a host-app capability. Intents should not be received by the platform intent client. - Beans.get(ManifestRegistry).registerCapability({type: 'some-host-app-capability'}, 'host-app'); - const platformClientIntentCaptor = new ObserveCaptor(); - Beans.get(PlatformIntentClient).observe$().subscribe(platformClientIntentCaptor); - const hostClientIntentCaptor = new ObserveCaptor(); - Beans.get(IntentClient).observe$().subscribe(hostClientIntentCaptor); - - // Issue the intent via host-app intent client. - await Beans.get(IntentClient).publish({type: 'some-host-app-capability'}); - - // Verify platform intent client not receiving the intent. - expect(platformClientIntentCaptor.getLastValue()).toBeUndefined(); - expect(hostClientIntentCaptor.getLastValue()).toBeDefined(); - - // Verify platform intent client not allowed to issue the intent. - await expectPromise(Beans.get(PlatformIntentClient).publish({type: 'some-host-app-capability'})).toReject(/NotQualifiedError/); - }); - }); - it('should allow multiple subscriptions to the same topic in the same client', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); const receiver1$ = Beans.get(MessageClient).observe$('topic').pipe(publishReplay(1)) as ConnectableObservable>; const receiver2$ = Beans.get(MessageClient).observe$('topic').pipe(publishReplay(1)) as ConnectableObservable>; @@ -729,9 +755,16 @@ describe('Messaging', () => { }); it('should allow multiple subscriptions to the same intent in the same client', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'xyz'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'xyz'}, + ], + }, + }, + applications: [], + }); const receiver1$ = Beans.get(IntentClient).observe$().pipe(publishReplay(1)) as ConnectableObservable>; const receiver2$ = Beans.get(IntentClient).observe$().pipe(publishReplay(1)) as ConnectableObservable>; @@ -800,9 +833,7 @@ describe('Messaging', () => { }); it('should receive a message once regardless of the number of subscribers in the same client', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register two receivers Beans.get(MessageClient).observe$('topic').subscribe(); @@ -822,9 +853,7 @@ describe('Messaging', () => { }); it('should dispatch a retained message only to the newly subscribed subscriber', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); const messageCollector1 = new ObserveCaptor(); const messageCollector2 = new ObserveCaptor(); @@ -838,7 +867,7 @@ describe('Messaging', () => { Beans.get(MessageClient).observe$('myhome/:room/temperature').subscribe(messageCollector2); // Publish the retained message - await Beans.get(PlatformMessageClient).publish('myhome/livingroom/temperature', '25°C', {retain: true}); + await Beans.get(MessageClient).publish('myhome/livingroom/temperature', '25°C', {retain: true}); await messageCollector1.waitUntilEmitCount(1); expectMessage(messageCollector1.getLastValue()).toMatch({ @@ -912,9 +941,16 @@ describe('Messaging', () => { }); it('should receive an intent once regardless of the number of subscribers in the same client', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'xyz'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'xyz'}, + ], + }, + }, + applications: [], + }); // Register two intent handlers Beans.get(IntentClient).observe$().subscribe(); @@ -934,9 +970,7 @@ describe('Messaging', () => { }); it('should receive an intent for a capability having an exact qualifier', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability const capabilityId = await Beans.get(ManifestService).registerCapability({type: 'view', qualifier: {entity: 'person', id: '5'}}); @@ -953,9 +987,7 @@ describe('Messaging', () => { }); it('should receive an intent for a capability having an asterisk qualifier', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability const capabilityId = await Beans.get(ManifestService).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}); @@ -972,9 +1004,7 @@ describe('Messaging', () => { }); it('should receive an intent for a capability having an optional qualifier', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability const capabilityId = await Beans.get(ManifestService).registerCapability({type: 'view', qualifier: {entity: 'person', id: '?'}}); @@ -993,43 +1023,31 @@ describe('Messaging', () => { }); it('should allow tracking the subscriptions on a topic', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); // Subscribe and wait until the initial subscription count, which is 0, is reported. const subscriberCountCaptor = new ObserveCaptor(); - Beans.get(PlatformMessageClient).subscriberCount$('some-topic').subscribe(subscriberCountCaptor); + Beans.get(MessageClient).subscriberCount$('some-topic').subscribe(subscriberCountCaptor); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe().unsubscribe(); - const subscription2 = Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(); - const subscription3 = Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(); + Beans.get(MessageClient).observe$('some-topic').subscribe().unsubscribe(); + const subscription2 = Beans.get(MessageClient).observe$('some-topic').subscribe(); + const subscription3 = Beans.get(MessageClient).observe$('some-topic').subscribe(); subscription2.unsubscribe(); subscription3.unsubscribe(); await expectEmissions(subscriberCountCaptor).toEqual([0, 1, 0, 1, 2, 1, 0]); }); - it('should set message headers about the sender (platform)', async () => { - await MicrofrontendPlatform.startHost([]); - - await Beans.get(PlatformMessageClient).publish('some-topic', 'body', {retain: true}); - - const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); - - await headersCaptor.waitUntilEmitCount(1); - expect(headersCaptor.getLastValue().get(MessageHeaders.ClientId)).toBeDefined(); - expect(headersCaptor.getLastValue().get(MessageHeaders.AppSymbolicName)).toEqual(PLATFORM_SYMBOLIC_NAME); - }); - - it('should set message headers about the sender (host-app)', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + it('should set message headers about the sender', async () => { + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); await Beans.get(MessageClient).publish('some-topic', 'body', {retain: true}); const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headersCaptor); await headersCaptor.waitUntilEmitCount(1); expect(headersCaptor.getLastValue().get(MessageHeaders.ClientId)).toBeDefined(); @@ -1037,14 +1055,15 @@ describe('Messaging', () => { }); it('should deliver custom headers in retained message', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); await Beans.get(MessageClient).publish('some-topic', 'body', {retain: true, headers: new Map().set('custom-header', 'some-value')}); const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headersCaptor); await headersCaptor.waitUntilEmitCount(1); expect(headersCaptor.getLastValue().get(MessageHeaders.ClientId)).toBeDefined(); @@ -1053,9 +1072,10 @@ describe('Messaging', () => { }); it('should deliver the client-id from the publisher when receiving a retained message upon subscription', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); await waitForCondition((): boolean => Beans.get(ClientRegistry).getByApplication('host-app').length > 0, 1000); const senderClientId = Beans.get(ClientRegistry).getByApplication('host-app')[0].id; @@ -1063,64 +1083,70 @@ describe('Messaging', () => { await Beans.get(MessageClient).publish('some-topic', 'body', {retain: true}); const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headersCaptor); await headersCaptor.waitUntilEmitCount(1); expect(headersCaptor.getLastValue().get(MessageHeaders.ClientId)).toEqual(senderClientId); }); it('should throw if the topic of a message to publish is empty, `null` or `undefined`, or contains wildcard segments', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformMessageClient).publish('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).publish(null)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).publish(undefined)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).publish('')).toThrowError(/IllegalTopicError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(MessageClient).publish('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).publish(null)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).publish(undefined)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).publish('')).toThrowError(/IllegalTopicError/); }); it('should throw if the topic of a request is empty, `null` or `undefined`, or contains wildcard segments', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformMessageClient).request$('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).request$(null)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).request$(undefined)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).request$('')).toThrowError(/IllegalTopicError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(MessageClient).request$('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).request$(null)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).request$(undefined)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).request$('')).toThrowError(/IllegalTopicError/); }); it('should throw if the topic to observe the subscriber count is empty, `null` or `undefined`, or contains wildcard segments', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformMessageClient).subscriberCount$('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).subscriberCount$(null)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).subscriberCount$(undefined)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).subscriberCount$('')).toThrowError(/IllegalTopicError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(MessageClient).subscriberCount$('myhome/:room/temperature')).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).subscriberCount$(null)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).subscriberCount$(undefined)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).subscriberCount$('')).toThrowError(/IllegalTopicError/); }); it('should throw if the topic to observe is empty, `null` or `undefined`', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformMessageClient).observe$(null)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).observe$(undefined)).toThrowError(/IllegalTopicError/); - expect(() => Beans.get(PlatformMessageClient).observe$('')).toThrowError(/IllegalTopicError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(MessageClient).observe$(null)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).observe$(undefined)).toThrowError(/IllegalTopicError/); + expect(() => Beans.get(MessageClient).observe$('')).toThrowError(/IllegalTopicError/); }); it('should throw if the qualifier of an intent contains wildcard characters', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformIntentClient).publish({type: 'type', qualifier: {entity: 'person', id: '*'}})).toThrowError(/IllegalQualifierError/); - expect(() => Beans.get(PlatformIntentClient).publish({type: 'type', qualifier: {entity: 'person', id: '?'}})).toThrowError(/IllegalQualifierError/); - expect(() => Beans.get(PlatformIntentClient).publish({type: 'type', qualifier: {entity: '*', id: '*'}})).toThrowError(/IllegalQualifierError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(IntentClient).publish({type: 'type', qualifier: {entity: 'person', id: '*'}})).toThrowError(/IllegalQualifierError/); + expect(() => Beans.get(IntentClient).publish({type: 'type', qualifier: {entity: 'person', id: '?'}})).toThrowError(/IllegalQualifierError/); + expect(() => Beans.get(IntentClient).publish({type: 'type', qualifier: {entity: '*', id: '*'}})).toThrowError(/IllegalQualifierError/); }); it('should throw if the qualifier of an intent request contains wildcard characters', async () => { - await MicrofrontendPlatform.startHost([]); - expect(() => Beans.get(PlatformIntentClient).request$({type: 'type', qualifier: {entity: 'person', id: '*'}})).toThrowError(/IllegalQualifierError/); - expect(() => Beans.get(PlatformIntentClient).request$({type: 'type', qualifier: {entity: 'person', id: '?'}})).toThrowError(/IllegalQualifierError/); - expect(() => Beans.get(PlatformIntentClient).request$({type: 'type', qualifier: {entity: '*', id: '*'}})).toThrowError(/IllegalQualifierError/); + await MicrofrontendPlatform.startHost({applications: []}); + + expect(() => Beans.get(IntentClient).request$({type: 'type', qualifier: {entity: 'person', id: '*'}})).toThrowError(/IllegalQualifierError/); + expect(() => Beans.get(IntentClient).request$({type: 'type', qualifier: {entity: 'person', id: '?'}})).toThrowError(/IllegalQualifierError/); + expect(() => Beans.get(IntentClient).request$({type: 'type', qualifier: {entity: '*', id: '*'}})).toThrowError(/IllegalQualifierError/); }); it('should prevent overriding platform specific message headers [pub/sub]', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headersCaptor); - await Beans.get(PlatformMessageClient).publish('some-topic', 'payload', { + await Beans.get(MessageClient).publish('some-topic', 'payload', { headers: new Map() .set(MessageHeaders.Timestamp, 'should-not-be-set') .set(MessageHeaders.ClientId, 'should-not-be-set') @@ -1137,12 +1163,12 @@ describe('Messaging', () => { }); it('should prevent overriding platform specific message headers [request/reply]', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const headersCaptor = new ObserveCaptor(headersExtractFn); - Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(headersCaptor); + Beans.get(MessageClient).observe$('some-topic').subscribe(headersCaptor); - Beans.get(PlatformMessageClient).request$('some-topic', 'payload', { + Beans.get(MessageClient).request$('some-topic', 'payload', { headers: new Map() .set(MessageHeaders.Timestamp, 'should-not-be-set') .set(MessageHeaders.ClientId, 'should-not-be-set') @@ -1159,9 +1185,15 @@ describe('Messaging', () => { }); it('should prevent overriding platform specific intent message headers [pub/sub]', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); const headersCaptor = new ObserveCaptor(headersExtractFn); Beans.get(IntentClient).observe$().subscribe(headersCaptor); @@ -1181,9 +1213,15 @@ describe('Messaging', () => { }); it('should prevent overriding platform specific intent message headers [request/reply]', async () => { - const manifestUrl = serveManifest({name: 'Host Application', capabilities: [{type: 'some-capability'}]}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [{type: 'some-capability'}], + }, + }, + applications: [], + }); const headersCaptor = new ObserveCaptor(headersExtractFn); Beans.get(IntentClient).observe$().subscribe(headersCaptor); @@ -1205,14 +1243,14 @@ describe('Messaging', () => { describe('takeUntilUnsubscribe operator', () => { it('should complete the source observable when all subscribers unsubscribed', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); - const subscription = Beans.get(PlatformMessageClient).observe$('some-topic').subscribe(); + const subscription = Beans.get(MessageClient).observe$('some-topic').subscribe(); await waitUntilSubscriberCount('some-topic', 1); const testee = new Subject() .pipe( - takeUntilUnsubscribe('some-topic', PlatformMessageClient), + takeUntilUnsubscribe('some-topic'), timeoutWith(new Date(Date.now() + 2000), throwError('[SpecTimeoutError] Timeout elapsed.')), ) .toPromise(); @@ -1224,11 +1262,11 @@ describe('Messaging', () => { }); it('should complete the source observable immediately when no subscriber is subscribed', async () => { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); const testee = new Subject() .pipe( - takeUntilUnsubscribe('nobody-subscribed-to-this-topic', PlatformMessageClient), + takeUntilUnsubscribe('nobody-subscribed-to-this-topic'), timeoutWith(new Date(Date.now() + 500), throwError('[SpecTimeoutError] Timeout elapsed.')), ) .toPromise(); @@ -1240,15 +1278,21 @@ describe('Messaging', () => { describe('intents with params', () => { it('should allow issuing an intent with parameters', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [{name: 'param1', required: true}], - requiredParams: ['param2'], // legacy notation - }], + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [{name: 'param1', required: true}], + requiredParams: ['param2'], // legacy notation + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); // publish const observeCaptor = new ObserveCaptor(paramsExtractFn); @@ -1258,15 +1302,21 @@ describe('Messaging', () => { }); it('should allow issuing an intent without passing optional parameters', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [{name: 'param1', required: false}], - optionalParams: ['param2'], // legacy notation - }], + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [{name: 'param1', required: false}], + optionalParams: ['param2'], // legacy notation + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); // publish const observeCaptor = new ObserveCaptor(paramsExtractFn); @@ -1276,15 +1326,21 @@ describe('Messaging', () => { }); it('should reject an intent if parameters are missing', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [{name: 'param1', required: true}], - requiredParams: ['param2'], // legacy notation - }], + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [{name: 'param1', required: true}], + requiredParams: ['param2'], // legacy notation + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); // publish await expectPromise(Beans.get(IntentClient).publish({type: 'capability', params: new Map().set('param1', 'value1')})).toReject(/\[ParamMismatchError].*missingParams=\[param2]/); @@ -1293,31 +1349,44 @@ describe('Messaging', () => { }); it('should reject an intent if it includes non-specified parameter', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [{name: 'param1', required: false}], - }], + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [{name: 'param1', required: false}], + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); // publish await expectPromise(Beans.get(IntentClient).publish({type: 'capability', params: new Map().set('param1', 'value1').set('param2', 'value2')})).toReject(/\[ParamMismatchError].*unexpectedParams=\[param2]/); }); it('should map deprecated params to their substitutes, if declared', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [ - {name: 'param1', required: false, deprecated: {useInstead: 'param2'}}, - {name: 'param2', required: true}, - ], - }], + await MicrofrontendPlatform.startHost({ + host: { + symbolicName: 'host-app', + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [ + {name: 'param1', required: false, deprecated: {useInstead: 'param2'}}, + {name: 'param2', required: true}, + ], + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); // publish const observeCaptor = new ObserveCaptor(paramsExtractFn); @@ -1331,18 +1400,24 @@ describe('Messaging', () => { }); it('should make deprecated params optional', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [ - {name: 'param1', required: false, deprecated: true}, - {name: 'param2', required: false, deprecated: {useInstead: 'param3'}}, - {name: 'param3', required: true}, - ], - }], + await MicrofrontendPlatform.startHost({ + host: { + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [ + {name: 'param1', required: false, deprecated: true}, + {name: 'param2', required: false, deprecated: {useInstead: 'param3'}}, + {name: 'param3', required: true}, + ], + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); const observeCaptor = new ObserveCaptor(paramsExtractFn); Beans.get(IntentClient).observe$({type: 'capability'}).subscribe(observeCaptor); @@ -1365,20 +1440,27 @@ describe('Messaging', () => { }); it('should log deprecation warning when passing deprecated params in an intent', async () => { - const manifestUrl = serveManifest({ - name: 'Host Application', capabilities: [{ - type: 'capability', - params: [ - {name: 'param1', required: false, deprecated: {message: 'DEPRECATION NOTICE'}}, - {name: 'param2', required: false, deprecated: {message: 'DEPRECATION NOTICE', useInstead: 'param5'}}, - {name: 'param3', required: false, deprecated: {useInstead: 'param5'}}, - {name: 'param4', required: false, deprecated: true}, - {name: 'param5', required: false}, - ], - }], + await MicrofrontendPlatform.startHost({ + host: { + symbolicName: 'host-app', + manifest: { + name: 'Host Application', + capabilities: [ + { + type: 'capability', + params: [ + {name: 'param1', required: false, deprecated: {message: 'DEPRECATION NOTICE'}}, + {name: 'param2', required: false, deprecated: {message: 'DEPRECATION NOTICE', useInstead: 'param5'}}, + {name: 'param3', required: false, deprecated: {useInstead: 'param5'}}, + {name: 'param4', required: false, deprecated: true}, + {name: 'param5', required: false}, + ], + }, + ], + }, + }, + applications: [], }); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app'}); const observeCaptor = new ObserveCaptor(paramsExtractFn); Beans.get(IntentClient).observe$({type: 'capability'}).subscribe(observeCaptor); @@ -1479,7 +1561,7 @@ function expectMessage(actual: TopicMessage): {toMatch: (expected: TopicMessage) */ async function waitUntilSubscriberCount(topic: string, expectedCount: number, options?: {timeout?: number}): Promise { const timeout = Defined.orElse(options && options.timeout, 1000); - await Beans.opt(PlatformMessageClient).subscriberCount$(topic) + await Beans.opt(MessageClient).subscriberCount$(topic) .pipe( first(count => count === expectedCount), timeoutWith(new Date(Date.now() + timeout), throwError('[SpecTimeoutError] Timeout elapsed.')), diff --git "a/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265intent-client.ts" "b/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265intent-client.ts" index c669d83f..31c40ee2 100644 --- "a/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265intent-client.ts" +++ "b/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265intent-client.ts" @@ -21,8 +21,7 @@ import {MessageHandler} from './message-handler'; export class ɵIntentClient implements IntentClient { - constructor(private readonly _brokerGateway: BrokerGateway) { - } + private readonly _brokerGateway = Beans.get(BrokerGateway); public publish(intent: Intent, body?: T, options?: IntentOptions): Promise { assertExactQualifier(intent.qualifier); diff --git "a/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265message-client.ts" "b/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265message-client.ts" index 36f49ad2..0d60abf6 100644 --- "a/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265message-client.ts" +++ "b/projects/scion/microfrontend-platform/src/lib/client/messaging/\311\265message-client.ts" @@ -18,8 +18,7 @@ import {MessageHandler} from './message-handler'; export class ɵMessageClient implements MessageClient { - constructor(private readonly _brokerGateway: BrokerGateway) { - } + private readonly _brokerGateway = Beans.get(BrokerGateway); public publish(topic: string, message?: T, options?: PublishOptions): Promise { assertTopic(topic, {allowWildcardSegments: false}); diff --git a/projects/scion/microfrontend-platform/src/lib/client/micro-application-config.ts b/projects/scion/microfrontend-platform/src/lib/client/micro-application-config.ts deleted file mode 100644 index d402a86e..00000000 --- a/projects/scion/microfrontend-platform/src/lib/client/micro-application-config.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -/** - * Configures a micro application to connect to the platform. - * - * @category Platform - */ -export abstract class MicroApplicationConfig { - /** - * Specifies the symbolic name of the micro application. The micro application must be registered in the host application under this symbol. - */ - public symbolicName!: string; - /** - * Configures interaction with the message broker. - */ - public messaging?: { - /** - * Disables messaging, useful in tests when not connecting to the platform host. By default, messaging is enabled. - */ - enabled?: boolean; - /** - * Specifies the maximal time (in milliseconds) to wait until the message broker is discovered on platform startup. If the broker is not discovered within - * the specified time, platform startup fails with an error. By default, a timeout of 10s is used. - */ - brokerDiscoverTimeout?: number; - /** - * Specifies the maximal time (in milliseconds) to wait to receive dispatch confirmation when sending a message. Otherwise, the Promise for sending the - * message rejects with an error. By default, a timeout of 10s is used. - */ - deliveryTimeout?: number; - }; -} diff --git a/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-move-event-dispatcher.ts b/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-move-event-dispatcher.ts index 333d8ea9..8570e957 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-move-event-dispatcher.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-move-event-dispatcher.ts @@ -30,6 +30,8 @@ export class MouseMoveEventDispatcher implements PreDestroy { private _dispatcherId = UUID.randomUUID(); constructor() { + // IMPORTANT: In Angular applications, the platform should be started outside the Angular zone in order to avoid excessive change detection cycles + // of platform-internal subscriptions to global DOM events. For that reason, we subscribe to `document.mousemove` events in the dispatcher's constructor. this.produceSynthEvents(); this.consumeSynthEvents(); } @@ -37,10 +39,6 @@ export class MouseMoveEventDispatcher implements PreDestroy { /** * Produces synth events from native 'mousemove' events and publishes them on the message bus. * It allows event dispatchers in other documents to consume these events and publish them on the document's event bus. - * - * IMPORTANT: - * Always subscribe to DOM events during event dispatcher construction. Event dispatchers are eagerly created on platform startup. - * Frameworks like Angular usually connect to the platform outside their change detection zone in order to avoid triggering change detection for unrelated DOM events. */ private produceSynthEvents(): void { fromEvent(document, 'mousemove') diff --git a/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-up-event-dispatcher.ts b/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-up-event-dispatcher.ts index 09fc821a..595c6033 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-up-event-dispatcher.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/mouse-event/mouse-up-event-dispatcher.ts @@ -29,6 +29,8 @@ export class MouseUpEventDispatcher implements PreDestroy { private _dispatcherId = UUID.randomUUID(); constructor() { + // IMPORTANT: In Angular applications, the platform should be started outside the Angular zone in order to avoid excessive change detection cycles + // of platform-internal subscriptions to global DOM events. For that reason, we subscribe to `document.mouseup` events in the dispatcher's constructor. this.produceSynthEvents(); this.consumeSynthEvents(); } @@ -36,10 +38,6 @@ export class MouseUpEventDispatcher implements PreDestroy { /** * Produces synth events from native 'mouseup' events and publishes them on the message bus. * It allows event dispatchers in other documents to consume these events and publish them on the document's event bus. - * - * IMPORTANT: - * Always subscribe to DOM events during event dispatcher construction. Event dispatchers are eagerly created on platform startup. - * Frameworks like Angular usually connect to the platform outside their change detection zone in order to avoid triggering change detection for unrelated DOM events. */ private produceSynthEvents(): void { fromEvent(document, 'mouseup') diff --git a/projects/scion/microfrontend-platform/src/lib/client/public_api.ts b/projects/scion/microfrontend-platform/src/lib/client/public_api.ts index aa808f60..240425f2 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/public_api.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/public_api.ts @@ -11,7 +11,7 @@ /** * Entry point for all public APIs of this package. */ -export {MicroApplicationConfig} from './micro-application-config'; +export * from './connect-options'; export * from './router-outlet/public_api'; export * from './context/public_api'; export * from './focus/public_api'; diff --git a/projects/scion/microfrontend-platform/src/lib/client/router-outlet/named-parameter-substitution.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/router-outlet/named-parameter-substitution.spec.ts index 8c68f66b..5ab88b33 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/router-outlet/named-parameter-substitution.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/router-outlet/named-parameter-substitution.spec.ts @@ -10,24 +10,20 @@ import {MicrofrontendPlatform} from '../../microfrontend-platform'; import {take} from 'rxjs/operators'; -import {PlatformMessageClient} from '../../host/platform-message-client'; import {RouterOutlets} from './router-outlet.element'; import {OutletRouter} from './outlet-router'; import {NavigationOptions} from './metadata'; -import {ApplicationConfig} from '../../host/platform-config'; -import {expectPromise, serveManifest} from '../../spec.util.spec'; +import {expectPromise} from '../../spec.util.spec'; import {UUID} from '@scion/toolkit/uuid'; import {mapToBody} from '../../messaging.model'; import {Beans} from '@scion/toolkit/bean-manager'; +import {MessageClient} from '../messaging/message-client'; describe('OutletRouter', () => { describe('Named parameter substitution', () => { - beforeAll(async () => { - const platformConfig: ApplicationConfig[] = [{symbolicName: 'host-client-app', manifestUrl: serveManifest({name: 'Host Client App'})}]; - await MicrofrontendPlatform.startHost(platformConfig, {symbolicName: 'host-client-app'}); - }); + beforeAll(async () => await MicrofrontendPlatform.startHost({applications: []})); afterAll(async () => await MicrofrontendPlatform.destroy()); describe('absolute URL (hash-based routing)', () => testSubstitution('http://localhost:4200/#/', {expectedBasePath: 'http://localhost:4200/#/'})); @@ -158,7 +154,7 @@ describe('OutletRouter', () => { // Navigate to the given URL await Beans.get(OutletRouter).navigate(url, {...navigationOptions, outlet}); // Lookup the navigated URL - return Beans.get(PlatformMessageClient).observe$(RouterOutlets.urlTopic(outlet)).pipe(take(1), mapToBody()).toPromise(); + return Beans.get(MessageClient).observe$(RouterOutlets.urlTopic(outlet)).pipe(take(1), mapToBody()).toPromise(); } }); }); diff --git a/projects/scion/microfrontend-platform/src/lib/client/router-outlet/relative-path-resolver.spec.ts b/projects/scion/microfrontend-platform/src/lib/client/router-outlet/relative-path-resolver.spec.ts index 27565e6d..3aa84dcc 100644 --- a/projects/scion/microfrontend-platform/src/lib/client/router-outlet/relative-path-resolver.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/client/router-outlet/relative-path-resolver.spec.ts @@ -14,7 +14,7 @@ import {Beans} from '@scion/toolkit/bean-manager'; describe('RelativePathResolver', () => { - beforeEach(async () => await MicrofrontendPlatform.startPlatform((): void => void (Beans.register(RelativePathResolver)))); + beforeEach(async () => await MicrofrontendPlatform.startPlatform(async () => void (Beans.register(RelativePathResolver)))); afterEach(async () => await MicrofrontendPlatform.destroy()); describe('hash-based routing', () => { diff --git a/projects/scion/microfrontend-platform/src/lib/host/activator/activator-installer.ts b/projects/scion/microfrontend-platform/src/lib/host/activator/activator-installer.ts index 5e48d8a8..a4a5672c 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/activator/activator-installer.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/activator/activator-installer.ts @@ -8,7 +8,6 @@ * SPDX-License-Identifier: EPL-2.0 */ import {Activator, PlatformCapabilityTypes} from '../../platform.model'; -import {PlatformManifestService} from '../../client/manifest-registry/platform-manifest-service'; import {catchError, filter, mergeMapTo, take} from 'rxjs/operators'; import {ApplicationRegistry} from '../application-registry'; import {OutletRouter} from '../../client/router-outlet/outlet-router'; @@ -16,7 +15,6 @@ import {SciRouterOutletElement} from '../../client/router-outlet/router-outlet.e import {Arrays, Maps} from '@scion/toolkit/util'; import {UUID} from '@scion/toolkit/uuid'; import {Logger} from '../../logger'; -import {PlatformMessageClient} from '../platform-message-client'; import {MessageHeaders} from '../../messaging.model'; import {EMPTY} from 'rxjs'; import {PlatformState} from '../../platform-state'; @@ -25,6 +23,8 @@ import {PlatformStateRef} from '../../platform-state-ref'; import {ProgressMonitor} from '../progress-monitor/progress-monitor'; import {ActivatorLoadProgressMonitor} from '../progress-monitor/progress-monitors'; import {timeoutIfPresent} from '../../operators'; +import {ManifestService} from '../../client/manifest-registry/manifest-service'; +import {MessageClient} from '../../client/messaging/message-client'; /** * Activates micro applications which provide an activator capability. @@ -38,7 +38,7 @@ export class ActivatorInstaller implements Initializer { public async init(): Promise { // Lookup activators. - const activators: Activator[] = await Beans.get(PlatformManifestService).lookupCapabilities$({type: PlatformCapabilityTypes.Activator}) + const activators: Activator[] = await Beans.get(ManifestService).lookupCapabilities$({type: PlatformCapabilityTypes.Activator}) .pipe(take(1)) .toPromise(); @@ -90,7 +90,7 @@ export class ActivatorInstaller implements Initializer { const activatorLoadTimeout = Beans.get(ApplicationRegistry).getApplication(appSymbolicName)!.activatorLoadTimeout; const readinessPromises: Promise[] = activators .reduce((acc, activator) => acc.concat(Arrays.coerce(activator.properties.readinessTopics)), new Array()) // concat readiness topics - .map(readinessTopic => Beans.get(PlatformMessageClient).observe$(readinessTopic) + .map(readinessTopic => Beans.get(MessageClient).observe$(readinessTopic) .pipe( filter(msg => msg.headers.get(MessageHeaders.AppSymbolicName) === appSymbolicName), timeoutIfPresent(activatorLoadTimeout), diff --git a/projects/scion/microfrontend-platform/src/lib/host/application-config.ts b/projects/scion/microfrontend-platform/src/lib/host/application-config.ts new file mode 100644 index 00000000..3c8d53a9 --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/host/application-config.ts @@ -0,0 +1,52 @@ +/** + * Describes how to register an application in the platform. + * + * @category Platform + */ +export interface ApplicationConfig { + /** + * Unique symbolic name of this micro application. + * + * The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens. + */ + symbolicName: string; + /** + * URL to the application manifest. + */ + manifestUrl: string; + /** + * Maximum time (in milliseconds) that the host waits until the manifest for this application is loaded. + * + * If set, overrides the global timeout as configured in {@link MicrofrontendPlatformConfig.manifestLoadTimeout}. + */ + manifestLoadTimeout?: number; + /** + * Maximum time (in milliseconds) for this application to signal readiness. + * + * If activating this application takes longer, the host logs an error and continues startup. + * If set, overrides the global timeout as configured in {@link MicrofrontendPlatformConfig.activatorLoadTimeout}. + */ + activatorLoadTimeout?: number; + /** + * Excludes this micro application from registration, e.g. to not register it in a specific environment. + */ + exclude?: boolean; + /** + * Controls whether this micro application can interact with private capabilities of other micro applications. + * + * By default, scope check is enabled. Disabling scope check is discouraged. + */ + scopeCheckDisabled?: boolean; + /** + * Controls whether this micro application can interact with the capabilities of other apps without having to declare respective intentions. + * + * By default, intention check is enabled. Disabling intention check is strongly discouraged. + */ + intentionCheckDisabled?: boolean; + /** + * Controls whether this micro application can register and unregister intentions dynamically at runtime. + * + * By default, this API is disabled. Enabling this API is strongly discouraged. + */ + intentionRegisterApiDisabled?: boolean; +} diff --git a/projects/scion/microfrontend-platform/src/lib/host/application-registry.spec.ts b/projects/scion/microfrontend-platform/src/lib/host/application-registry.spec.ts index fa0b702b..09be7638 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/application-registry.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/application-registry.spec.ts @@ -10,12 +10,12 @@ import {ApplicationRegistry} from './application-registry'; import {MicrofrontendPlatform} from '../microfrontend-platform'; import {ManifestRegistry} from './manifest-registry/manifest-registry'; -import {PlatformMessageClient} from './platform-message-client'; import {ɵMessageClient} from '../client/messaging/ɵmessage-client'; import {ɵManifestRegistry} from './manifest-registry/ɵmanifest-registry'; -import {NullBrokerGateway} from '../client/messaging/broker-gateway'; +import {BrokerGateway, NullBrokerGateway} from '../client/messaging/broker-gateway'; import {Beans} from '@scion/toolkit/bean-manager'; -import {PlatformConfig} from './platform-config'; +import {MicrofrontendPlatformConfig} from './microfrontend-platform-config'; +import {MessageClient} from '../client/messaging/message-client'; describe('ApplicationRegistry', () => { @@ -23,11 +23,12 @@ describe('ApplicationRegistry', () => { beforeEach(async () => { await MicrofrontendPlatform.destroy(); - await MicrofrontendPlatform.startPlatform(() => { - Beans.register(PlatformConfig); + await MicrofrontendPlatform.startPlatform(async () => { + Beans.register(MicrofrontendPlatformConfig); Beans.register(ApplicationRegistry); Beans.register(ManifestRegistry, {useClass: ɵManifestRegistry, eager: true}); - Beans.register(PlatformMessageClient, {useFactory: () => new ɵMessageClient(new NullBrokerGateway())}); + Beans.register(BrokerGateway, {useClass: NullBrokerGateway}); + Beans.register(MessageClient, {useClass: ɵMessageClient}); }); registry = Beans.get(ApplicationRegistry); }); diff --git a/projects/scion/microfrontend-platform/src/lib/host/application-registry.ts b/projects/scion/microfrontend-platform/src/lib/host/application-registry.ts index 92662933..3af9c02c 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/application-registry.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/application-registry.ts @@ -8,13 +8,14 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {Application, ApplicationManifest} from '../platform.model'; +import {Application, Manifest} from '../platform.model'; import {Defined} from '@scion/toolkit/util'; import {Urls} from '../url.util'; -import {ApplicationConfig, PlatformConfig} from './platform-config'; +import {MicrofrontendPlatformConfig} from './microfrontend-platform-config'; import {ManifestRegistry} from './manifest-registry/manifest-registry'; import {Beans} from '@scion/toolkit/bean-manager'; import {Logger} from '../logger'; +import {ApplicationConfig} from './application-config'; /** * Registry with all registered applications. @@ -32,7 +33,7 @@ export class ApplicationRegistry { * * Throws an error if the application's symbolic name is not unique or contains illegal characters. */ - public registerApplication(applicationConfig: ApplicationConfig, manifest: ApplicationManifest): void { + public registerApplication(applicationConfig: ApplicationConfig, manifest: Manifest): void { Defined.orElseThrow(applicationConfig.symbolicName, () => Error('[ApplicationRegistrationError] Missing symbolic name')); Defined.orElseThrow(applicationConfig.manifestUrl, () => Error('[ApplicationRegistrationError] Missing manifest URL')); @@ -50,8 +51,8 @@ export class ApplicationRegistry { name: manifest.name, baseUrl: this.computeBaseUrl(applicationConfig, manifest), manifestUrl: Urls.newUrl(applicationConfig.manifestUrl, Urls.isAbsoluteUrl(applicationConfig.manifestUrl) ? applicationConfig.manifestUrl : window.origin).toString(), - manifestLoadTimeout: applicationConfig.manifestLoadTimeout ?? Beans.get(PlatformConfig).manifestLoadTimeout, - activatorLoadTimeout: applicationConfig.activatorLoadTimeout ?? Beans.get(PlatformConfig).activatorLoadTimeout, + manifestLoadTimeout: applicationConfig.manifestLoadTimeout ?? Beans.get(MicrofrontendPlatformConfig).manifestLoadTimeout, + activatorLoadTimeout: applicationConfig.activatorLoadTimeout ?? Beans.get(MicrofrontendPlatformConfig).activatorLoadTimeout, origin: Urls.newUrl(this.computeBaseUrl(applicationConfig, manifest)).origin, scopeCheckDisabled: Defined.orElse(applicationConfig.scopeCheckDisabled, false), intentionCheckDisabled: Defined.orElse(applicationConfig.intentionCheckDisabled, false), @@ -113,7 +114,7 @@ export class ApplicationRegistry { * - if base URL is not specified in the manifest, the origin from 'manifestUrl' is used as the base URL, or the origin from the current window if the 'manifestUrl' is relative * - if base URL has no trailing slash, adds a trailing slash */ - private computeBaseUrl(applicationConfig: ApplicationConfig, manifest: ApplicationManifest): string { + private computeBaseUrl(applicationConfig: ApplicationConfig, manifest: Manifest): string { const manifestURL = Urls.isAbsoluteUrl(applicationConfig.manifestUrl) ? Urls.newUrl(applicationConfig.manifestUrl) : Urls.newUrl(applicationConfig.manifestUrl, window.origin); if (!manifest.baseUrl) { diff --git a/projects/scion/microfrontend-platform/src/lib/host/focus/focus-tracker.ts b/projects/scion/microfrontend-platform/src/lib/host/focus/focus-tracker.ts index 51fd2a8d..63f0b985 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/focus/focus-tracker.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/focus/focus-tracker.ts @@ -7,10 +7,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {PlatformMessageClient} from '../platform-message-client'; import {BehaviorSubject, Subject} from 'rxjs'; import {distinctUntilChanged, map, takeUntil} from 'rxjs/operators'; -import {takeUntilUnsubscribe} from '../../client/messaging/message-client'; +import {MessageClient, takeUntilUnsubscribe} from '../../client/messaging/message-client'; import {MessageHeaders, TopicMessage} from '../../messaging.model'; import {runSafe} from '../../safe-runner'; import {PlatformTopics} from '../../ɵmessaging.model'; @@ -39,7 +38,7 @@ export class FocusTracker implements PreDestroy { * Monitors when a client gains the focus. */ private monitorFocusInEvents(): void { - Beans.get(PlatformMessageClient).observe$(PlatformTopics.FocusIn) + Beans.get(MessageClient).observe$(PlatformTopics.FocusIn) .pipe( map(event => event.headers.get(MessageHeaders.ClientId)), distinctUntilChanged(), @@ -54,7 +53,7 @@ export class FocusTracker implements PreDestroy { * Replies to 'focus-within' requests. */ private replyToIsFocusWithinRequests(): void { - Beans.get(PlatformMessageClient).observe$(PlatformTopics.IsFocusWithin) + Beans.get(MessageClient).observe$(PlatformTopics.IsFocusWithin) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const clientId = request.headers.get(MessageHeaders.ClientId); @@ -64,11 +63,11 @@ export class FocusTracker implements PreDestroy { .pipe( map(focusOwner => this.isFocusWithin(clientId, focusOwner)), distinctUntilChanged(), - takeUntilUnsubscribe(replyTo, PlatformMessageClient), + takeUntilUnsubscribe(replyTo), takeUntil(this._destroy$), ) .subscribe((isFocusWithin: boolean) => { // eslint-disable-line rxjs/no-nested-subscribe - Beans.get(PlatformMessageClient).publish(replyTo, isFocusWithin); + Beans.get(MessageClient).publish(replyTo, isFocusWithin); }); })); } @@ -79,8 +78,6 @@ export class FocusTracker implements PreDestroy { private isFocusWithin(clientId: string, focusOwner: Client | undefined): boolean { const clientWindow = Defined.orElseThrow(Beans.get(ClientRegistry).getByClientId(clientId), () => Error(`[NullClientError] No client registered under '${clientId}'.`)).window; for (let client = focusOwner; client !== undefined; client = this.getParentClient(client)) { - // Compare against the window instead of the client id because in the host app the - // {@link MessageClient} and {@link PlatformMessageClient} share the same window if (client.window === clientWindow) { return true; } diff --git a/projects/scion/microfrontend-platform/src/lib/host/host-application-config-provider.ts b/projects/scion/microfrontend-platform/src/lib/host/host-application-config-provider.ts new file mode 100644 index 00000000..20b3c02a --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/host/host-application-config-provider.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018-2020 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {APP_IDENTITY, Manifest} from '../platform.model'; +import {PlatformState} from '../platform-state'; +import {Beans} from '@scion/toolkit/bean-manager'; +import {PlatformStateRef} from '../platform-state-ref'; +import {ApplicationConfig} from './application-config'; +import {HostConfig} from './host-config'; + +/** + * Creates the {@link ApplicationConfig} for the host app. + */ +export function createHostApplicationConfig(hostConfig: HostConfig | undefined): ApplicationConfig { + return { + symbolicName: Beans.get(APP_IDENTITY), + manifestUrl: provideHostManifestUrl(hostConfig?.manifest), + scopeCheckDisabled: hostConfig?.scopeCheckDisabled, + intentionCheckDisabled: hostConfig?.intentionCheckDisabled, + intentionRegisterApiDisabled: hostConfig?.intentionRegisterApiDisabled, + }; +} + +function provideHostManifestUrl(hostManifest: string | Manifest | undefined): string { + if (typeof hostManifest === 'string') { + return hostManifest; // URL specified + } + + return serveHostManifest(hostManifest || {name: 'Host Application'}); +} + +function serveHostManifest(manifest: Manifest): string { + const url = URL.createObjectURL(new Blob([JSON.stringify(manifest)], {type: 'application/json'})); + Beans.get(PlatformStateRef).whenState(PlatformState.Stopped).then(() => URL.revokeObjectURL(url)); + return url; +} diff --git a/projects/scion/microfrontend-platform/src/lib/host/host-config.ts b/projects/scion/microfrontend-platform/src/lib/host/host-config.ts new file mode 100644 index 00000000..9f3233cf --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/host/host-config.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2018-2020 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {Manifest} from '../platform.model'; + +/** + * Configures the interaction of the host application with the platform. + * + * As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions. + * + * @category Platform + */ +export abstract class HostConfig { + /** + * Symbolic name of the host. If not set, 'host' is used as the symbolic name of the host. + * + * The symbolic name must be unique and contain only lowercase alphanumeric characters and hyphens. + */ + public abstract symbolicName?: string; + /** + * The manifest of the host. + * + * The manifest can be passed either as an {@link Manifest object literal} or specified as a URL to be loaded over the network. + * Providing a manifest lets the host contribute capabilities or declare intentions. + */ + public abstract readonly manifest?: Manifest | string; + /** + * Controls whether the host can interact with private capabilities of other micro applications. + * + * By default, scope check is enabled. Disabling scope check is discouraged. + */ + public abstract readonly scopeCheckDisabled?: boolean; + /** + * Controls whether the host can interact with the capabilities of other apps without having to declare respective intentions. + * + * By default, intention check is enabled. Disabling intention check is strongly discouraged. + */ + public abstract readonly intentionCheckDisabled?: boolean; + /** + * Controls whether the host can register and unregister intentions dynamically at runtime. + * + * By default, this API is disabled. Enabling this API is strongly discouraged. + */ + public abstract readonly intentionRegisterApiDisabled?: boolean; + /** + * Maximum time (in milliseconds) that the platform waits to receive dispatch confirmation for messages sent by the host until rejecting the publishing Promise. + * By default, a timeout of 10s is used. + */ + public abstract readonly messageDeliveryTimeout?: number; +} diff --git a/projects/scion/microfrontend-platform/src/lib/host/host-manifest-interceptor.ts b/projects/scion/microfrontend-platform/src/lib/host/host-manifest-interceptor.ts new file mode 100644 index 00000000..fb1176b8 --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/host/host-manifest-interceptor.ts @@ -0,0 +1,85 @@ +import {Intention, Manifest, PlatformCapabilityTypes} from '../platform.model'; +import {MicrofrontendPlatformConfig} from './microfrontend-platform-config'; +import {Beans} from '@scion/toolkit/bean-manager'; + +/** + * Hook to intercept the host manifest before it is registered in the platform. + * + * If integrating the platform in a library, you may need to intercept the manifest of the host in order to introduce library-specific behavior. + * + * You can register the interceptor in the bean manager, as follows: + * + * ```ts + * Beans.register(HostManifestInterceptor, {useClass: YourInterceptor, multi: true}); + * ``` + * + * The interceptor may look as following: + * ```ts + * class YourInterceptor implements HostManifestInterceptor { + * + * public intercept(hostManifest: Manifest): void { + * hostManifest.intentions = [ + * ...hostManifest.intentions || [], + * provideMicrofrontendIntention(), + * ]; + * hostManifest.capabilities = [ + * ...hostManifest.capabilities || [], + * provideMessageBoxCapability(), + * ]; + * } + * } + * + * function provideMicrofrontendIntention(): Intention { + * return { + * type: 'microfrontend', + * qualifier: {'*': '*'}, + * }; + * } + * + * function provideMessageBoxCapability(): Capability { + * return { + * type: 'messagebox', + * qualifier: {}, + * private: false, + * description: 'Allows displaying a simple message to the user.', + * }; + * } + * + * ``` + */ +export abstract class HostManifestInterceptor { + + /** + * Allows modifying the host manifest before it is registered in the platform, e.g., to register capabilities or intentions. + */ + public abstract intercept(hostManifest: Manifest): void; +} + +/** + * Intercepts the host manifest, registering platform-specific intentions and capabilities. + * + * @internal + */ +export class ɵHostManifestInterceptor implements HostManifestInterceptor { + + public intercept(hostManifest: Manifest): void { + hostManifest.intentions = [ + ...hostManifest.intentions || [], + ...provideActivatorIntentionIfEnabled(), + ]; + } +} + +/** + * Provides a wildcard activator intention for the platform to read activator capabilities for installing activator microfrontends. + */ +function provideActivatorIntentionIfEnabled(): Intention[] { + const activatorApiDisabled = Beans.get(MicrofrontendPlatformConfig).activatorApiDisabled ?? false; + if (activatorApiDisabled) { + return []; + } + return [{ + type: PlatformCapabilityTypes.Activator, + qualifier: {'*': '*'}, + }]; +} diff --git a/projects/scion/microfrontend-platform/src/lib/host/host-platform-app-provider.ts b/projects/scion/microfrontend-platform/src/lib/host/host-platform-app-provider.ts deleted file mode 100644 index 94bd1462..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/host-platform-app-provider.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {ApplicationManifest, Intention, PlatformCapabilityTypes} from '../platform.model'; -import {ApplicationConfig, PlatformFlags} from './platform-config'; -import {PLATFORM_SYMBOLIC_NAME} from './platform.constants'; -import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; - -/** - * Provides the {@link ApplicationConfig} for the platform app running in the platform host. - * - * This app is used by the platform in the host to connect to the message broker and to provide platform specific capabilities, - * or to issue intents in the name of the platform. - * - * @ignore - */ -export class HostPlatformAppProvider implements PreDestroy { - - public readonly appConfig: ApplicationConfig; - - constructor() { - const manifest: ApplicationManifest = { - name: 'SCION Microfrontend Platform', - capabilities: [], - intentions: [ - ...provideActivatorApiIntentions(), - ], - }; - - this.appConfig = { - symbolicName: PLATFORM_SYMBOLIC_NAME, - manifestUrl: URL.createObjectURL(new Blob([JSON.stringify(manifest)], {type: 'application/json'})), - }; - } - - public preDestroy(): void { - URL.revokeObjectURL(this.appConfig.manifestUrl); - } -} - -/** - * If the 'Activator API' is enabled, authorize the host platform app to read activators from the manifest registry. - * - * @ignore - */ -function provideActivatorApiIntentions(): Intention[] { - return Beans.get(PlatformFlags).activatorApiDisabled ? [] : [{type: PlatformCapabilityTypes.Activator, qualifier: {'*': '*'}}]; -} diff --git a/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.spec.ts b/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.spec.ts index 67cf0efa..8ada07af 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.spec.ts @@ -10,7 +10,7 @@ import {HttpClient} from './http-client'; import {MicrofrontendPlatform} from '../microfrontend-platform'; -import {ApplicationManifest} from '../platform.model'; +import {Manifest} from '../platform.model'; import {ApplicationRegistry} from './application-registry'; import {Logger} from '../logger'; import {Beans} from '@scion/toolkit/bean-manager'; @@ -28,7 +28,7 @@ describe('ManifestCollector', () => { httpClientSpy.fetch .withArgs('http://www.app-2/manifest').and.returnValue(okAnswer({body: {name: 'application-2', intentions: [], capabilities: []}, delay: 120})) .withArgs('http://www.app-3/manifest').and.returnValue(okAnswer({body: {name: 'application-3', intentions: [], capabilities: []}, delay: 30})) - .and.callFake((arg) => fetch(arg)); // fetches the manifest of 'scion-platform' host app + .and.callFake((arg) => fetch(arg)); // fetches the manifest of the host app Beans.register(HttpClient, {useValue: httpClientSpy}); // mock {Logger} @@ -36,11 +36,16 @@ describe('ManifestCollector', () => { Beans.register(Logger, {useValue: loggerSpy}); // start the platform - await MicrofrontendPlatform.startHost([ - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'application-1'})}, - {symbolicName: 'app-2', manifestUrl: 'http://www.app-2/manifest'}, - {symbolicName: 'app-3', manifestUrl: 'http://www.app-3/manifest'}, - ], {symbolicName: 'app-1'}); + await MicrofrontendPlatform.startHost({ + host: { + symbolicName: 'app-1', + manifest: serveManifest({name: 'application-1'}), + }, + applications: [ + {symbolicName: 'app-2', manifestUrl: 'http://www.app-2/manifest'}, + {symbolicName: 'app-3', manifestUrl: 'http://www.app-3/manifest'}, + ], + }); // assert application registrations expect(Beans.get(ApplicationRegistry).getApplication('app-1').name).toEqual('application-1'); @@ -63,7 +68,7 @@ describe('ManifestCollector', () => { .withArgs('http://www.app-2/manifest').and.returnValue(nokAnswer({status: 500, delay: 100})) .withArgs('http://www.app-3/manifest').and.returnValue(okAnswer({body: {name: 'application-3', intentions: [], capabilities: []}, delay: 600})) .withArgs('http://www.app-4/manifest').and.returnValue(nokAnswer({status: 502, delay: 200})) - .and.callFake((arg) => fetch(arg)); // fetches the manifest of 'scion-platform' host app + .and.callFake((arg) => fetch(arg)); // fetches the manifest of the host app Beans.register(HttpClient, {useValue: httpClientSpy}); @@ -72,12 +77,14 @@ describe('ManifestCollector', () => { Beans.register(Logger, {useValue: loggerSpy}); // start the platform - await MicrofrontendPlatform.startHost([ - {symbolicName: 'app-1', manifestUrl: 'http://www.app-1/manifest'}, - {symbolicName: 'app-2', manifestUrl: 'http://www.app-2/manifest'}, - {symbolicName: 'app-3', manifestUrl: 'http://www.app-3/manifest'}, - {symbolicName: 'app-4', manifestUrl: 'http://www.app-4/manifest'}, - ]); + await MicrofrontendPlatform.startHost({ + applications: [ + {symbolicName: 'app-1', manifestUrl: 'http://www.app-1/manifest'}, + {symbolicName: 'app-2', manifestUrl: 'http://www.app-2/manifest'}, + {symbolicName: 'app-3', manifestUrl: 'http://www.app-3/manifest'}, + {symbolicName: 'app-4', manifestUrl: 'http://www.app-4/manifest'}, + ], + }); // assert application registrations expect(Beans.get(ApplicationRegistry).getApplication('app-1').name).toEqual('application-1'); @@ -95,7 +102,7 @@ describe('ManifestCollector', () => { .withArgs('http://www.app-2/manifest').and.returnValue(okAnswer({body: {name: 'application-2', intentions: [], capabilities: []}, delay: 400})) .withArgs('http://www.app-3/manifest').and.returnValue(okAnswer({body: {name: 'application-3', intentions: [], capabilities: []}, delay: 600})) // greater than the global manifestLoadTimeout => expect failure .withArgs('http://www.app-4/manifest').and.returnValue(okAnswer({body: {name: 'application-4', intentions: [], capabilities: []}, delay: 600})) // less then than the app-specific manifestLoadTimeout => expect success - .and.callFake((arg) => fetch(arg)); // fetches the manifest of 'scion-platform' host app + .and.callFake((arg) => fetch(arg)); // fetches the manifest of the host app Beans.register(HttpClient, {useValue: httpClientSpy}); @@ -105,7 +112,7 @@ describe('ManifestCollector', () => { // start the platform await MicrofrontendPlatform.startHost({ - apps: [ + applications: [ {symbolicName: 'app-1', manifestUrl: 'http://www.app-1/manifest', manifestLoadTimeout: 300}, // app-specific timeout {symbolicName: 'app-2', manifestUrl: 'http://www.app-2/manifest'}, {symbolicName: 'app-3', manifestUrl: 'http://www.app-3/manifest'}, @@ -125,7 +132,7 @@ describe('ManifestCollector', () => { }); }); -function okAnswer(answer: {body: ApplicationManifest; delay: number}): Promise> { +function okAnswer(answer: {body: Manifest; delay: number}): Promise> { const response: Partial = { ok: true, json: (): Promise => Promise.resolve(answer.body), diff --git a/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.ts b/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.ts index 47988ed3..cdf49e53 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/manifest-collector.ts @@ -8,21 +8,18 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {PlatformConfigLoader} from './platform-config-loader'; -import {Defined} from '@scion/toolkit/util'; -import {ApplicationConfig, PlatformConfig, PlatformFlags} from './platform-config'; +import {MicrofrontendPlatformConfig} from './microfrontend-platform-config'; import {ApplicationRegistry} from './application-registry'; import {HttpClient} from './http-client'; import {Logger} from '../logger'; -import {HostPlatformAppProvider} from './host-platform-app-provider'; -import {PlatformMessageClient} from '../host/platform-message-client'; -import {PlatformTopics} from '../ɵmessaging.model'; import {Beans, Initializer} from '@scion/toolkit/bean-manager'; -import {Runlevel} from '../platform-state'; import {ProgressMonitor} from './progress-monitor/progress-monitor'; import {ManifestLoadProgressMonitor} from './progress-monitor/progress-monitors'; import {from} from 'rxjs'; import {timeoutIfPresent} from '../operators'; +import {APP_IDENTITY, Manifest, ɵAPP_CONFIG} from '../platform.model'; +import {HostManifestInterceptor} from './host-manifest-interceptor'; +import {ApplicationConfig} from './application-config'; /** * Collects manifests of registered applications. @@ -33,29 +30,11 @@ import {timeoutIfPresent} from '../operators'; export class ManifestCollector implements Initializer { public async init(): Promise { - // Load platform config - const platformConfig: PlatformConfig = await Beans.get(PlatformConfigLoader).load(); - Defined.orElseThrow(platformConfig, () => Error('[PlatformConfigError] No platform config provided.')); - Defined.orElseThrow(platformConfig.apps, () => Error('[PlatformConfigError] Missing \'apps\' property in platform config. Did you forget to register applications?')); - Beans.register(PlatformFlags, {useValue: this.computePlatformFlags(platformConfig)}); - Beans.register(PlatformConfig, {useValue: platformConfig}); - - // Load application manifests - await Promise.all(this.fetchAndRegisterManifests(platformConfig)); - - // Wait until messaging is enabled to avoid running into a publish timeout. - Beans.whenRunlevel(Runlevel.Two).then(() => { - Beans.get(PlatformMessageClient).publish(PlatformTopics.PlatformProperties, platformConfig.properties || {}, {retain: true}); - Beans.get(PlatformMessageClient).publish(PlatformTopics.Applications, Beans.get(ApplicationRegistry).getApplications(), {retain: true}); - }); + await Promise.all(this.fetchAndRegisterManifests()); } - private fetchAndRegisterManifests(platformConfig: PlatformConfig): Promise[] { - const appConfigs: ApplicationConfig[] = new Array() - .concat(Beans.get(HostPlatformAppProvider).appConfig) - .concat(platformConfig.apps) - .filter(appConfig => !appConfig.exclude); - + private fetchAndRegisterManifests(): Promise[] { + const appConfigs = Beans.all(ɵAPP_CONFIG); const monitor = Beans.get(ManifestLoadProgressMonitor); if (!appConfigs.length) { monitor.done(); @@ -63,10 +42,10 @@ export class ManifestCollector implements Initializer { } const subMonitors = monitor.splitEven(appConfigs.length); - return appConfigs.map((appConfig, index) => this.fetchAndRegisterManifest(platformConfig, appConfig, subMonitors[index])); + return appConfigs.map((appConfig, index) => this.fetchAndRegisterManifest(appConfig, subMonitors[index])); } - private async fetchAndRegisterManifest(platformConfig: PlatformConfig, appConfig: ApplicationConfig, monitor: ProgressMonitor): Promise { + private async fetchAndRegisterManifest(appConfig: ApplicationConfig, monitor: ProgressMonitor): Promise { if (!appConfig.manifestUrl) { Beans.get(Logger).error(`[AppConfigError] Failed to fetch manifest for application '${appConfig.symbolicName}'. Manifest URL must not be empty.`); return; @@ -74,7 +53,7 @@ export class ManifestCollector implements Initializer { try { const response = await from(Beans.get(HttpClient).fetch(appConfig.manifestUrl)) - .pipe(timeoutIfPresent(appConfig.manifestLoadTimeout ?? platformConfig.manifestLoadTimeout)) + .pipe(timeoutIfPresent(appConfig.manifestLoadTimeout ?? Beans.get(MicrofrontendPlatformConfig).manifestLoadTimeout)) .toPromise(); if (!response.ok) { @@ -82,8 +61,15 @@ export class ManifestCollector implements Initializer { return; } - Beans.get(ApplicationRegistry).registerApplication(appConfig, await response.json()); - Beans.get(Logger).info(`Application '${appConfig.symbolicName}' registered as micro application in the platform.`); + const manifest: Manifest = await response.json(); + + // Let the host manifest be intercepted before registering it in the platform, for example by libraries integrating the SCION Microfrontend Platform, e.g., to allow the programmatic registration of capabilities or intentions. + if (appConfig.symbolicName === Beans.get(APP_IDENTITY)) { + Beans.all(HostManifestInterceptor).forEach(interceptor => interceptor.intercept(manifest)); + } + + Beans.get(ApplicationRegistry).registerApplication(appConfig, manifest); + Beans.get(Logger).info(`Registered application '${appConfig.symbolicName}' in the SCION Microfrontend Platform.`); } catch (error) { // The Promise returned from fetch() won’t reject on HTTP error status even if the response is an HTTP 404 or 500. @@ -95,11 +81,4 @@ export class ManifestCollector implements Initializer { monitor.done(); } } - - private computePlatformFlags(platformConfig: PlatformConfig): PlatformFlags { - return { - ...platformConfig.platformFlags, - activatorApiDisabled: Defined.orElse(platformConfig.platformFlags && platformConfig.platformFlags.activatorApiDisabled, false), - }; - } } diff --git a/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/manifest-registry.spec.ts b/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/manifest-registry.spec.ts index 1029f76e..0e67c076 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/manifest-registry.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/manifest-registry.spec.ts @@ -9,7 +9,6 @@ */ import {MicrofrontendPlatform} from '../../microfrontend-platform'; import {expectEmissions, installLoggerSpies, readConsoleLog, serveManifest} from '../../spec.util.spec'; -import {ApplicationConfig} from '../platform-config'; import {Beans} from '@scion/toolkit/bean-manager'; import {ManifestRegistry} from './manifest-registry'; import {Capability} from '../../platform.model'; @@ -30,21 +29,22 @@ describe('ManifestRegistry', () => { describe('hasIntention', () => { it('should error if not passing an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); expect(() => Beans.get(ManifestRegistry).hasIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app')).toThrowError(/IllegalQualifierError/); }); it(`should have an implicit intention for a capability having an exact qualifier ({entity: 'person', id: '5'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '5'}}, 'app-1'); @@ -62,12 +62,13 @@ describe('ManifestRegistry', () => { }); it(`should have an implicit intention for a capability having an asterisk wildcard qualifier ({entity: 'person', id: '*'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'app-1'); @@ -84,12 +85,13 @@ describe('ManifestRegistry', () => { }); it(`should have an implicit intention for a capability having an optional qualifier ({entity: 'person', id: '?'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '?'}}, 'app-1'); @@ -106,12 +108,13 @@ describe('ManifestRegistry', () => { }); it(`should match an intention having an exact qualifier ({entity: 'person', id: '5'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register intention await Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '5'}}, 'app-1'); @@ -129,12 +132,13 @@ describe('ManifestRegistry', () => { }); it(`should match an intention having an asterisk wildcard qualifier ({entity: 'person', id: '*'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register intention await Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'app-1'); @@ -151,12 +155,13 @@ describe('ManifestRegistry', () => { }); it(`should match an intention having an any-more wildcard (**) qualifier ({entity: 'person', '*': '*'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register intention await Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {'entity': 'person', '*': '*'}}, 'app-1'); @@ -174,12 +179,13 @@ describe('ManifestRegistry', () => { }); it(`should match an intention having an optional wildcard (?) qualifier ({entity: 'person', id: '?'})`, async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register intention await Beans.get(ManifestRegistry).registerIntention({type: 'view', qualifier: {entity: 'person', id: '?'}}, 'app-1'); @@ -199,10 +205,10 @@ describe('ManifestRegistry', () => { describe('resolveCapabilitiesByIntent', () => { it('should error if not passing an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); expect(() => Beans.get(ManifestRegistry).resolveCapabilitiesByIntent({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'host-app')).toThrowError(/IllegalQualifierError/); }); @@ -210,12 +216,13 @@ describe('ManifestRegistry', () => { describe('implicit intention', () => { it('should resolve to own private capability having an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '5'}}, 'app-1'); @@ -230,12 +237,13 @@ describe('ManifestRegistry', () => { }); it('should resolve to own private capability having an asterisk wildcard (*) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}}, 'app-1'); @@ -250,12 +258,13 @@ describe('ManifestRegistry', () => { }); it('should resolve to own private capability having an optional wildcard (?) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '?'}}, 'app-1'); @@ -270,12 +279,13 @@ describe('ManifestRegistry', () => { }); it('should not resolve to private capabilities of other applications', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capabilities of app-1 Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: 'id1'}, private: true}, 'app-1'); @@ -293,12 +303,13 @@ describe('ManifestRegistry', () => { }); it('should not resolve to public capabilities of other applications', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capabilities of app-1 Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: 'id1'}, private: false}, 'app-1'); @@ -319,12 +330,13 @@ describe('ManifestRegistry', () => { describe('explicit intention', () => { it('should resolve to public foreign capability having an exact qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability of app-1 const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '5'}, private: false}, 'app-1'); @@ -336,12 +348,13 @@ describe('ManifestRegistry', () => { }); it('should resolve to public foreign capability having an asterisk wildcard (*) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability of app-1 const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '*'}, private: false}, 'app-1'); @@ -353,12 +366,13 @@ describe('ManifestRegistry', () => { }); it('should resolve to public foreign capability having an optional wildcard (?) in the qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capability of app-1 const capabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: '?'}, private: false}, 'app-1'); @@ -371,12 +385,13 @@ describe('ManifestRegistry', () => { }); it('should resolve to public (but not private) capabilities of other apps', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, - {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [ + {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'App 1'})}, + {symbolicName: 'app-2', manifestUrl: serveManifest({name: 'App 2'})}, + ], + }); // Register capabilities of app-1 const publicCapabilityId = Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {entity: 'person', id: 'public'}, private: false}, 'app-1'); @@ -396,10 +411,10 @@ describe('ManifestRegistry', () => { }); it('should not allow registering a capability using the any-more wildcard (**) in its qualifier', async () => { - const registeredApps: ApplicationConfig[] = [ - {symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); expect(() => Beans.get(ManifestRegistry).registerCapability({type: 'view', qualifier: {'entity': 'person', '*': '*'}}, 'host-app')).toThrowError(`[CapabilityRegisterError] Asterisk wildcard ('*') not allowed in the qualifier key.`); }); @@ -407,10 +422,10 @@ describe('ManifestRegistry', () => { describe('Capability Params', () => { it('should register params and support legacy param declaration (via manifest)', async () => { // Register capability via manifest - const registeredApps: ApplicationConfig[] = [ - { + await MicrofrontendPlatform.startHost({ + host: { symbolicName: 'host-app', - manifestUrl: serveManifest({ + manifest: { name: 'Host', capabilities: [ { @@ -423,10 +438,10 @@ describe('ManifestRegistry', () => { optionalParams: ['param4'], // deprecated; expect legacy support }, ], - }), + }, }, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + applications: [], + }); // Assert deprecation warning expect(readConsoleLog('warn')).toEqual(jasmine.arrayContaining([ @@ -451,8 +466,10 @@ describe('ManifestRegistry', () => { }); it('should register params and support legacy param declaration (via ManifestService)', async () => { - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({ + host: {symbolicName: 'host-app'}, + applications: [], + }); // Register capability via ManifestServie const capabilityId = await Beans.get(ManifestService).registerCapability({ @@ -488,11 +505,10 @@ describe('ManifestRegistry', () => { }); it('should error if params forget to declare whether they are required or optional (via manifest)', async () => { - // Register capability via manifest - const registeredApps: ApplicationConfig[] = [ - { + await MicrofrontendPlatform.startHost({ + host: { symbolicName: 'host-app', - manifestUrl: serveManifest({ + manifest: { name: 'Host', capabilities: [ { @@ -502,18 +518,18 @@ describe('ManifestRegistry', () => { ], }, ], - }), + }, }, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + applications: [], + }); + expect(readConsoleLog('error', {filter: /CapabilityParamError/, projectFn: (call: CallInfo) => (call.args[1] as Error)?.message})).toEqual(jasmine.arrayContaining([ `[CapabilityParamError] Parameter 'param' must be explicitly defined as required or optional.`, ])); }); it('should error if params forget to declare whether they are required or optional (via ManifestService)', async () => { - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability via ManifestServie await expectAsync(Beans.get(ManifestService).registerCapability({ @@ -525,10 +541,10 @@ describe('ManifestRegistry', () => { it('should error if deprecated params are required (via manifest)', async () => { // Register capability via manifest - const registeredApps: ApplicationConfig[] = [ - { + await MicrofrontendPlatform.startHost({ + host: { symbolicName: 'host-app', - manifestUrl: serveManifest({ + manifest: { name: 'Host', capabilities: [ { @@ -538,10 +554,11 @@ describe('ManifestRegistry', () => { ], }, ], - }), + }, }, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + applications: [], + }); + expect(readConsoleLog('error', {filter: /CapabilityParamError/, projectFn: (call: CallInfo) => (call.args[1] as Error)?.message})).toEqual(jasmine.arrayContaining([ `[CapabilityParamError] Deprecated parameters must be optional, not required. Alternatively, deprecated parameters can define a mapping to a required parameter via the 'useInstead' property. [param='param1']`, ])); @@ -549,10 +566,10 @@ describe('ManifestRegistry', () => { it('should error if deprecated params, which declare a substitute, are required (via manifest)', async () => { // Register capability via manifest - const registeredApps: ApplicationConfig[] = [ - { + await MicrofrontendPlatform.startHost({ + host: { symbolicName: 'host-app', - manifestUrl: serveManifest({ + manifest: { name: 'Host', capabilities: [ { @@ -563,18 +580,18 @@ describe('ManifestRegistry', () => { ], }, ], - }), + }, }, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + applications: [], + }); + expect(readConsoleLog('error', {filter: /CapabilityParamError/, projectFn: (call: CallInfo) => (call.args[1] as Error)?.message})).toEqual(jasmine.arrayContaining([ `[CapabilityParamError] Deprecated parameters must be optional, not required. Alternatively, deprecated parameters can define a mapping to a required parameter via the 'useInstead' property. [param='param1']`, ])); }); it('should error if deprecated params are required (via ManifestService)', async () => { - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability via ManifestServie await expectAsync(Beans.get(ManifestService).registerCapability({ @@ -585,8 +602,7 @@ describe('ManifestRegistry', () => { }); it('should error if deprecated params, which declare a substitute, are required (via ManifestService)', async () => { - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability via ManifestServie await expectAsync(Beans.get(ManifestService).registerCapability({ @@ -601,10 +617,10 @@ describe('ManifestRegistry', () => { it('should error if deprecated params declare an invalid substitute (via manifest)', async () => { // Register capability via manifest - const registeredApps: ApplicationConfig[] = [ - { + await MicrofrontendPlatform.startHost({ + host: { symbolicName: 'host-app', - manifestUrl: serveManifest({ + manifest: { name: 'Host', capabilities: [ { @@ -616,18 +632,18 @@ describe('ManifestRegistry', () => { ], }, ], - }), + }, }, - ]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + applications: [], + }); + expect(readConsoleLog('error', {filter: /CapabilityParamError/, projectFn: (call: CallInfo) => (call.args[1] as Error)?.message})).toEqual(jasmine.arrayContaining([ `[CapabilityParamError] The deprecated parameter 'param1' defines an invalid substitute 'paramX'. Valid substitutes are: [param2,param3]`, ])); }); it('should error if deprecated params declare an invalid substitute (via ManifestService)', async () => { - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: serveManifest({name: 'Host'})}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Register capability via ManifestServie await expectAsync(Beans.get(ManifestService).registerCapability({ diff --git "a/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/\311\265manifest-registry.ts" "b/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/\311\265manifest-registry.ts" index f065d85a..10433b88 100644 --- "a/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/\311\265manifest-registry.ts" +++ "b/projects/scion/microfrontend-platform/src/lib/host/manifest-registry/\311\265manifest-registry.ts" @@ -13,9 +13,8 @@ import {sha256} from 'js-sha256'; import {ManifestObjectFilter, ManifestObjectStore} from './manifest-object-store'; import {defer, merge, of, Subject} from 'rxjs'; import {distinctUntilChanged, expand, mergeMapTo, take, takeUntil} from 'rxjs/operators'; -import {PlatformMessageClient} from '../platform-message-client'; import {Intent, MessageHeaders, ResponseStatusCodes, TopicMessage} from '../../messaging.model'; -import {takeUntilUnsubscribe} from '../../client/messaging/message-client'; +import {MessageClient, takeUntilUnsubscribe} from '../../client/messaging/message-client'; import {ApplicationRegistry} from '../application-registry'; import {runSafe} from '../../safe-runner'; import {filterArray} from '@scion/toolkit/operators'; @@ -103,7 +102,7 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { // use the first 7 digits of the capability hash as capability id const capabilityId = sha256(JSON.stringify({application: appSymbolicName, type: capability.type, ...capability.qualifier})).substr(0, 7); - const registeredCapability: Capability = { + const capabilityToRegister: Capability = { ...capability, qualifier: capability.qualifier ?? {}, params: coerceCapabilityParamDefinitions(capability, appSymbolicName), @@ -117,7 +116,7 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { }; // Register the capability. - this._capabilityStore.add(registeredCapability); + this._capabilityStore.add(capabilityToRegister); return capabilityId; } @@ -132,7 +131,7 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { // use the first 7 digits of the intention hash as intention id const intentionId = sha256(JSON.stringify({application: appSymbolicName, type: intention.type, ...intention.qualifier})).substr(0, 7); - const registeredIntention: Intention = { + const intentionToRegister: Intention = { ...intention, metadata: { id: intentionId, @@ -141,7 +140,7 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { }; // Register the intention. - this._intentionStore.add(registeredIntention); + this._intentionStore.add(intentionToRegister); return intentionId; } @@ -150,7 +149,7 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { } private installCapabilityRegisterRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.RegisterCapability) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.RegisterCapability) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -159,16 +158,16 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { try { const capabilityId = this.registerCapability(capability, appSymbolicName); - Beans.get(PlatformMessageClient).publish(replyTo, capabilityId, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, capabilityId, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); } catch (error) { - Beans.get(PlatformMessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + Beans.get(MessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); } })); } private installCapabilityUnregisterRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.UnregisterCapabilities) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.UnregisterCapabilities) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -177,16 +176,16 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { try { this.unregisterCapabilities(appSymbolicName, capabilityFilter); - Beans.get(PlatformMessageClient).publish(replyTo, undefined, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, undefined, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); } catch (error) { - Beans.get(PlatformMessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + Beans.get(MessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); } })); } private installIntentionRegisterRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.RegisterIntention) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.RegisterIntention) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -196,16 +195,16 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { try { assertIntentionRegisterApiEnabled(appSymbolicName); const intentionId = this.registerIntention(intention, appSymbolicName); - Beans.get(PlatformMessageClient).publish(replyTo, intentionId, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, intentionId, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); } catch (error) { - Beans.get(PlatformMessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + Beans.get(MessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); } })); } private installIntentionUnregisterRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.UnregisterIntentions) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.UnregisterIntentions) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -215,16 +214,16 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { try { assertIntentionRegisterApiEnabled(appSymbolicName); this.unregisterIntention(appSymbolicName, intentFilter); - Beans.get(PlatformMessageClient).publish(replyTo, undefined, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); + Beans.get(MessageClient).publish(replyTo, undefined, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.TERMINAL)}); } catch (error) { - Beans.get(PlatformMessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); + Beans.get(MessageClient).publish(replyTo, stringifyError(error), {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.ERROR)}); } })); } private installCapabilitiesLookupRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.LookupCapabilities) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.LookupCapabilities) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -240,16 +239,16 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { expand(() => registryChange$.pipe(take(1), mergeMapTo(finder$))), filterArray(capability => this.isApplicationQualifiedForCapability(appSymbolicName, capability)), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), - takeUntilUnsubscribe(replyTo, PlatformMessageClient), + takeUntilUnsubscribe(replyTo), ) .subscribe(capabilities => { // eslint-disable-line rxjs/no-nested-subscribe - Beans.get(PlatformMessageClient).publish(replyTo, capabilities, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); + Beans.get(MessageClient).publish(replyTo, capabilities, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); }); })); } private installIntentionsLookupRequestHandler(): void { - Beans.get(PlatformMessageClient).observe$(ManifestRegistryTopics.LookupIntentions) + Beans.get(MessageClient).observe$(ManifestRegistryTopics.LookupIntentions) .pipe(takeUntil(this._destroy$)) .subscribe((request: TopicMessage) => runSafe(() => { const replyTo = request.headers.get(MessageHeaders.ReplyTo); @@ -260,10 +259,10 @@ export class ɵManifestRegistry implements ManifestRegistry, PreDestroy { .pipe( expand(() => this._intentionStore.change$.pipe(take(1), mergeMapTo(finder$))), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), - takeUntilUnsubscribe(replyTo, PlatformMessageClient), + takeUntilUnsubscribe(replyTo), ) .subscribe(intentions => { // eslint-disable-line rxjs/no-nested-subscribe - Beans.get(PlatformMessageClient).publish(replyTo, intentions, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); + Beans.get(MessageClient).publish(replyTo, intentions, {headers: new Map().set(MessageHeaders.Status, ResponseStatusCodes.OK)}); }); })); } diff --git a/projects/scion/microfrontend-platform/src/lib/host/message-broker/client.registry.spec.ts b/projects/scion/microfrontend-platform/src/lib/host/message-broker/client.registry.spec.ts index 743cbb87..19982d93 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/message-broker/client.registry.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/message-broker/client.registry.spec.ts @@ -16,7 +16,7 @@ describe('ClientRegistry', () => { beforeEach(async () => { await MicrofrontendPlatform.destroy(); - await MicrofrontendPlatform.startPlatform((): void => void (Beans.register(ClientRegistry))); + await MicrofrontendPlatform.startPlatform(async () => void (Beans.register(ClientRegistry))); }); afterEach(async () => { diff --git a/projects/scion/microfrontend-platform/src/lib/host/message-broker/message-broker.ts b/projects/scion/microfrontend-platform/src/lib/host/message-broker/message-broker.ts index 5e3e7c0c..b7a0e00e 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/message-broker/message-broker.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/message-broker/message-broker.ts @@ -17,7 +17,6 @@ import {Defined} from '@scion/toolkit/util'; import {UUID} from '@scion/toolkit/uuid'; import {Logger} from '../../logger'; import {runSafe} from '../../safe-runner'; -import {PLATFORM_SYMBOLIC_NAME} from '../platform.constants'; import {TopicSubscriptionRegistry} from './topic-subscription.registry'; import {Client, ClientRegistry} from './client.registry'; import {RetainedMessageStore} from './retained-message-store'; @@ -25,7 +24,7 @@ import {TopicMatcher} from '../../topic-matcher.util'; import {chainInterceptors, IntentInterceptor, MessageInterceptor, PublishInterceptorChain} from './message-interception'; import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; import {Runlevel} from '../../platform-state'; -import {Capability, ParamDefinition} from '../../platform.model'; +import {APP_IDENTITY, Capability, ParamDefinition} from '../../platform.model'; import {bufferUntil} from '@scion/toolkit/operators'; import {ParamMatcher} from './param-matcher'; @@ -265,7 +264,7 @@ export class MessageBroker implements PreDestroy { body: count, headers: new Map() .set(MessageHeaders.MessageId, UUID.randomUUID()) - .set(MessageHeaders.AppSymbolicName, PLATFORM_SYMBOLIC_NAME), + .set(MessageHeaders.AppSymbolicName, Beans.get(APP_IDENTITY)), }); })); })); @@ -565,7 +564,7 @@ function sendTopicMessage(recipient: {gatewayWindow: Window; origin: string} headers.set(MessageHeaders.MessageId, UUID.randomUUID()); } if (!headers.has(MessageHeaders.AppSymbolicName)) { - headers.set(MessageHeaders.AppSymbolicName, PLATFORM_SYMBOLIC_NAME); + headers.set(MessageHeaders.AppSymbolicName, Beans.get(APP_IDENTITY)); } const target = recipient instanceof Client ? {gatewayWindow: recipient.gatewayWindow, origin: recipient.application.origin} : recipient; diff --git a/projects/scion/microfrontend-platform/src/lib/host/microfrontend-platform-config.ts b/projects/scion/microfrontend-platform/src/lib/host/microfrontend-platform-config.ts new file mode 100644 index 00000000..641d3321 --- /dev/null +++ b/projects/scion/microfrontend-platform/src/lib/host/microfrontend-platform-config.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018-2020 Swiss Federal Railways + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ + +import {ApplicationConfig} from './application-config'; +import {HostConfig} from './host-config'; + +/** + * Configures the platform and defines the micro applications running in the platform. + * + * @category Platform + */ +export abstract class MicrofrontendPlatformConfig { + /** + * Lists the micro applications able to connect to the platform to interact with other micro applications. + */ + public abstract readonly applications: ApplicationConfig[]; + /** + * Configures the interaction of the host application with the platform. + * + * As with micro applications, you can provide a manifest for the host, allowing the host to contribute capabilities and declare intentions. + */ + public abstract readonly host?: HostConfig; + /** + * Controls whether the Activator API is enabled. + * + * Activating the Activator API enables micro applications to contribute `activator` microfrontends. Activator microfrontends are loaded + * at platform startup for the entire lifecycle of the platform. An activator is a startup hook for micro applications to initialize + * or register message or intent handlers to provide functionality. + * + * By default, this API is enabled. + * + * @see {@link Activator} + */ + public abstract readonly activatorApiDisabled?: boolean; + /** + * Maximum time (in milliseconds) that the platform waits until the manifest of an application is loaded. + * You can set a different timeout per application via {@link ApplicationConfig.manifestLoadTimeout}. + * If not set, by default, the browser's HTTP fetch timeout applies. + * + * Consider setting this timeout if, for example, a web application firewall delays the responses of unavailable + * applications. + */ + public abstract readonly manifestLoadTimeout?: number; + /** + * Maximum time (in milliseconds) for each application to signal readiness. + * + * If specified and activating an application takes longer, the host logs an error and continues startup. + * Has no effect for applications which provide no activator(s) or are not configured to signal readiness. + * You can set a different timeout per application via {@link ApplicationConfig.activatorLoadTimeout}. + * + * By default, no timeout is set, meaning that if an app fails to signal readiness, e.g., due to an error, + * that app would block the host startup process indefinitely. It is therefore recommended to specify a + * timeout accordingly. + */ + public abstract readonly activatorLoadTimeout?: number; + /** + * Defines user-defined properties which can be read by micro applications via {@link PlatformPropertyService}. + */ + public abstract readonly properties?: { + [key: string]: any; + }; +} diff --git a/projects/scion/microfrontend-platform/src/lib/host/platform-config-loader.ts b/projects/scion/microfrontend-platform/src/lib/host/platform-config-loader.ts deleted file mode 100644 index 8ddd2bd6..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/platform-config-loader.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -import {PlatformConfig} from './platform-config'; - -/** - * Allows loading the config of applications running in the platform and platform properties asynchronously, e.g. from a backend. - * - * Using a {@link PlatformConfigLoader} allows a more flexible platform setup than providing a static config. For example, depending - * on the browser URL, a different config can be loaded, or the config can be read from a database from the backend. - * - * @category Platform - */ -export abstract class PlatformConfigLoader { - - /** - * Loads the platform config asynchronously. - */ - public abstract load(): Promise; -} diff --git a/projects/scion/microfrontend-platform/src/lib/host/platform-config.ts b/projects/scion/microfrontend-platform/src/lib/host/platform-config.ts deleted file mode 100644 index 9f25847c..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/platform-config.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ - -/** - * Configures the platform and defines the micro applications running in the platform. - * - * @category Platform - */ -export abstract class PlatformConfig { - /** - * Defines the micro applications running in the platform. - */ - public abstract readonly apps: ApplicationConfig[]; - /** - * Maximum time (in milliseconds) for each application to fetch its manifest. - * You can set a different timeout per application via {@link ApplicationConfig.manifestLoadTimeout}. - * If not set, by default, the browser's HTTP fetch timeout applies. - * - * Consider setting this timeout if, for example, a web application firewall delays the responses of unavailable - * applications. - */ - public abstract readonly manifestLoadTimeout?: number; - /** - * Maximum time (in milliseconds) that the host waits for each application to signal readiness. - * Has no effect for applications having no activator(s) or are not configured to signal readiness. - * You can set a different timeout per application via {@link ApplicationConfig.activatorLoadTimeout}. - * By default, no timeout is set. - * - * If an app fails to signal its readiness, e.g., due to an error, setting no timeout would cause - * that app to block the startup process indefinitely. - */ - public abstract readonly activatorLoadTimeout?: number; - /** - * Defines user-defined properties which can be read by micro applications via {@link PlatformPropertyService}. - */ - public abstract readonly properties?: { - [key: string]: any; - }; - /** - * Platform flags are settings and features that you can enable to change how the platform works. - */ - public abstract readonly platformFlags?: PlatformFlags; -} - -/** - * Describes how to register an application in the platform. - * - * @category Platform - */ -export interface ApplicationConfig { - /** - * Unique symbolic name of this micro application. - * - * Choose a short, lowercase name which contains alphanumeric characters and optionally dash characters. - */ - symbolicName: string; - /** - * URL to the application manifest. - */ - manifestUrl: string; - /** - * Maximum time (in milliseconds) that the host waits for this application to fetch its manifest. - * - * If set, overrides the global timeout as configured in {@link PlatformConfig.manifestLoadTimeout}. - */ - manifestLoadTimeout?: number; - /** - * Maximum time (in milliseconds) that the host waits for this application to signal readiness. - * - * If set, overrides the global timeout as configured in {@link PlatformConfig.activatorLoadTimeout}. - */ - activatorLoadTimeout?: number; - - /** - * Excludes this micro application from registration, e.g. to not register it in a specific environment. - */ - exclude?: boolean; - /** - * Sets whether or not this micro application can issue intents to private capabilities of other apps. - * - * By default, scope check is enabled. Disabling scope check is discouraged. - */ - scopeCheckDisabled?: boolean; - /** - * Sets whether or not this micro application can look up intentions or issue intents for which it has not declared a respective intention. - * - * By default, intention check is enabled. Disabling intention check is strongly discouraged. - */ - intentionCheckDisabled?: boolean; - /** - * Sets whether or not the API to manage intentions is disabled for this micro application. - * - * By default, this API is disabled. With this API enabled, the application can register and - * unregister intentions dynamically at runtime. Enabling this API is strongly discouraged. - */ - intentionRegisterApiDisabled?: boolean; -} - -/** - * Platform flags are settings and features that you can enable to change how the platform works. - * - * @category Platform - */ -export abstract class PlatformFlags { - /** - * Sets whether or not the API to provide application activators is disabled. - * - * By default, this API is enabled. - * - * @see {@link Activator} - */ - public activatorApiDisabled?: boolean; -} diff --git a/projects/scion/microfrontend-platform/src/lib/host/platform-intent-client.ts b/projects/scion/microfrontend-platform/src/lib/host/platform-intent-client.ts deleted file mode 100644 index e49dbd90..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/platform-intent-client.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -import {IntentClient} from '../client/messaging/intent-client'; - -/** - * Allows interacting with the Intention API on behalf of the platform app {@link PLATFORM_SYMBOLIC_NAME}. - * - * @category Platform - */ -export abstract class PlatformIntentClient extends IntentClient { -} diff --git a/projects/scion/microfrontend-platform/src/lib/host/platform-message-client.ts b/projects/scion/microfrontend-platform/src/lib/host/platform-message-client.ts deleted file mode 100644 index b7a5966f..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/platform-message-client.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -import {MessageClient} from '../client/messaging/message-client'; - -/** - * Message client used by the platform to send and receive messages. - * - * Messages are sent and received on behalf of the platform app {@link PLATFORM_SYMBOLIC_NAME}. - * - * @category Platform - */ -export abstract class PlatformMessageClient extends MessageClient { -} diff --git a/projects/scion/microfrontend-platform/src/lib/host/platform.constants.ts b/projects/scion/microfrontend-platform/src/lib/host/platform.constants.ts deleted file mode 100644 index 57f8072c..00000000 --- a/projects/scion/microfrontend-platform/src/lib/host/platform.constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2018-2020 Swiss Federal Railways - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -/** - * Symbolic name of the platform app running in the platform host. - * - * @category Platform - */ -export const PLATFORM_SYMBOLIC_NAME = 'scion-platform'; diff --git a/projects/scion/microfrontend-platform/src/lib/host/public_api.ts b/projects/scion/microfrontend-platform/src/lib/host/public_api.ts index cd59512e..51dca80e 100644 --- a/projects/scion/microfrontend-platform/src/lib/host/public_api.ts +++ b/projects/scion/microfrontend-platform/src/lib/host/public_api.ts @@ -11,8 +11,10 @@ /** * Entry point for all public APIs of this package. */ -export * from './platform-config'; -export * from './platform-config-loader'; +export * from './microfrontend-platform-config'; +export * from './host-config'; +export * from './application-config'; export * from './manifest-registry/public_api'; export * from './activator/public_api'; export * from './message-broker/message-interception'; +export {HostManifestInterceptor} from './host-manifest-interceptor'; diff --git a/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.spec.ts b/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.spec.ts index 52018452..5df2c279 100644 --- a/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.spec.ts @@ -9,10 +9,7 @@ */ import {MicrofrontendPlatform} from './microfrontend-platform'; -import {MessageClient} from './client/messaging/message-client'; -import {ApplicationConfig} from './host/platform-config'; -import {expectPromise, serveManifest, waitFor} from './spec.util.spec'; -import {PlatformMessageClient} from './host/platform-message-client'; +import {expectPromise, waitFor} from './spec.util.spec'; import {PlatformState} from './platform-state'; import {Beans} from '@scion/toolkit/bean-manager'; import {PlatformPropertyService} from './platform-property-service'; @@ -24,7 +21,7 @@ describe('MicrofrontendPlatform', () => { afterEach(async () => await MicrofrontendPlatform.destroy()); it('should report that the app is not connected to the platform host when the host platform is not found', async () => { - const startup = MicrofrontendPlatform.connectToHost({symbolicName: 'client-app', messaging: {brokerDiscoverTimeout: 250}}); + const startup = MicrofrontendPlatform.connectToHost('client-app', {brokerDiscoverTimeout: 250}); await expectPromise(startup).toReject(); await expect(await MicrofrontendPlatform.isConnectedToHost()).toBe(false); }); @@ -34,25 +31,23 @@ describe('MicrofrontendPlatform', () => { }); it('should report that the app is connected to the platform host when connected', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); await expect(await MicrofrontendPlatform.isConnectedToHost()).toBe(true); }); it('should enter state \'started\' when started', async () => { - const startup = MicrofrontendPlatform.connectToHost({symbolicName: 'A', messaging: {enabled: false}}); + const startup = MicrofrontendPlatform.connectToHost('A', {connect: false}); await expectPromise(startup).toResolve(); expect(MicrofrontendPlatform.state).toEqual(PlatformState.Started); }); it('should reject starting the client platform multiple times', async () => { - const startup = MicrofrontendPlatform.connectToHost({symbolicName: 'A', messaging: {enabled: false}}); + const startup = MicrofrontendPlatform.connectToHost('A', {connect: false}); await expectPromise(startup).toResolve(); try { - await MicrofrontendPlatform.connectToHost({symbolicName: 'A'}); + await MicrofrontendPlatform.connectToHost('A'); fail('expected \'MicrofrontendPlatform.forClient()\' to error'); } catch (error) { @@ -61,11 +56,11 @@ describe('MicrofrontendPlatform', () => { }); it('should reject starting the host platform multiple times', async () => { - const startup = MicrofrontendPlatform.startHost([]); + const startup = MicrofrontendPlatform.startHost({applications: []}); await expectPromise(startup).toResolve(); try { - await MicrofrontendPlatform.startHost([]); + await MicrofrontendPlatform.startHost({applications: []}); fail('expected \'MicrofrontendPlatform.startHost()\' to error'); } catch (error) { @@ -73,20 +68,6 @@ describe('MicrofrontendPlatform', () => { } }); - it('should register the `MessageClient` as alias for `PlatformMessageClient` when starting the host platform anonymously', async () => { - await MicrofrontendPlatform.startHost([]); - - expect(Beans.get(MessageClient)).toBe(Beans.get(PlatformMessageClient)); - }); - - it('should not register the `MessageClient` as alias for `PlatformMessageClient` when starting the host platform in the name of an app', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250, deliveryTimeout: 250}}); - - expect(Beans.get(MessageClient)).not.toBe(Beans.get(PlatformMessageClient)); - }); - it('should construct eager beans at platform startup', async () => { let constructed = false; @@ -96,7 +77,7 @@ describe('MicrofrontendPlatform', () => { } } - await MicrofrontendPlatform.startPlatform(() => { + await MicrofrontendPlatform.startPlatform(async () => { Beans.register(Bean, {eager: true}); }); @@ -115,7 +96,7 @@ describe('MicrofrontendPlatform', () => { Beans.registerInitializer({useFunction: () => void (log.push('executing initializer [runlevel=0]')), runlevel: 0}); Beans.registerInitializer({useFunction: () => void (log.push('executing initializer [runlevel=2]')), runlevel: 2}); - await MicrofrontendPlatform.startPlatform(() => { + await MicrofrontendPlatform.startPlatform(async () => { Beans.register(Bean, {eager: true}); }); @@ -245,20 +226,18 @@ describe('MicrofrontendPlatform', () => { Beans.registerInitializer(() => Promise.reject()); Beans.registerInitializer(() => Promise.resolve()); - await expectPromise(MicrofrontendPlatform.startPlatform()).toReject(/PlatformStartupError/); + await expectPromise(MicrofrontendPlatform.startPlatform()).toReject(/MicrofrontendPlatformStartupError/); }); it('should allow looking up platform properties from the host', async () => { await MicrofrontendPlatform.startHost({ - apps: [ - {symbolicName: 'app-1', manifestUrl: serveManifest({name: 'application-1'})}, - ], + applications: [], properties: { 'prop1': 'PROP1', 'prop2': 'PROP2', 'prop3': 'PROP3', }, - }, {symbolicName: 'app-1'}); + }); expect(Beans.get(PlatformPropertyService).properties()).toEqual(new Map() .set('prop1', 'PROP1') @@ -268,8 +247,6 @@ describe('MicrofrontendPlatform', () => { }); it('should not emit progress if not startet yet, report progress during startup, and complete after started [MicrofrontendPlatform.startHost]', async () => { - const manifestUrl = serveManifest({name: 'Host Application'}); - const registeredApps: ApplicationConfig[] = [{symbolicName: 'host-app', manifestUrl: manifestUrl}]; const captor1 = new ObserveCaptor(); const captor2 = new ObserveCaptor(); @@ -277,7 +254,7 @@ describe('MicrofrontendPlatform', () => { MicrofrontendPlatform.startupProgress$.subscribe(captor1); expect(captor1.getValues()).toEqual([]); // no emission - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Expect the progress to be 100% after the platform is started and the Observable to be completed. expect(captor1.getLastValue()).toEqual(100); @@ -289,7 +266,7 @@ describe('MicrofrontendPlatform', () => { MicrofrontendPlatform.startupProgress$.subscribe(captor2); expect(captor2.getValues()).toEqual([]); // no emission - await MicrofrontendPlatform.startHost(registeredApps, {symbolicName: 'host-app', messaging: {brokerDiscoverTimeout: 250}}); + await MicrofrontendPlatform.startHost({applications: []}); // Expect the progress to be 100% after the platform completed startup and the Observable to be completed. expect(captor2.getLastValue()).toEqual(100); @@ -305,7 +282,7 @@ describe('MicrofrontendPlatform', () => { expect(captor1.getValues()).toEqual([]); // no emission // start the platform - await MicrofrontendPlatform.connectToHost({symbolicName: 'A', messaging: {enabled: false}}); + await MicrofrontendPlatform.connectToHost('A', {connect: false}); // Expect the progress to be 100% after the platform completed startup and the Observable to be completed. expect(captor1.getLastValue()).toEqual(100); @@ -317,7 +294,7 @@ describe('MicrofrontendPlatform', () => { MicrofrontendPlatform.startupProgress$.subscribe(captor2); expect(captor2.getValues()).toEqual([]); // no emission - await MicrofrontendPlatform.connectToHost({symbolicName: 'A', messaging: {enabled: false}}); + await MicrofrontendPlatform.connectToHost('A', {connect: false}); // Expect the progress to be 100% after the platform completed startup and the Observable to be completed. expect(captor2.getLastValue()).toEqual(100); diff --git a/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.ts b/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.ts index 393cc53b..7663d1e5 100644 --- a/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.ts +++ b/projects/scion/microfrontend-platform/src/lib/microfrontend-platform.ts @@ -9,20 +9,15 @@ */ import {MessageClient} from './client/messaging/message-client'; import {IntentClient} from './client/messaging/intent-client'; -import {PlatformIntentClient} from './host/platform-intent-client'; import {ManifestRegistry} from './host/manifest-registry/manifest-registry'; import {ApplicationRegistry} from './host/application-registry'; -import {PlatformConfigLoader} from './host/platform-config-loader'; import {BehaviorSubject, from, Observable, Subject} from 'rxjs'; -import {MicroApplicationConfig} from './client/micro-application-config'; -import {ApplicationConfig, PlatformConfig} from './host/platform-config'; +import {ConnectOptions} from './client/connect-options'; +import {MicrofrontendPlatformConfig} from './host/microfrontend-platform-config'; import {PlatformPropertyService} from './platform-property-service'; import {ConsoleLogger, Logger} from './logger'; import {HttpClient} from './host/http-client'; import {ManifestCollector} from './host/manifest-collector'; -import {PlatformMessageClient} from './host/platform-message-client'; -import {PLATFORM_SYMBOLIC_NAME} from './host/platform.constants'; -import {Defined} from '@scion/toolkit/util'; import {MessageBroker} from './host/message-broker/message-broker'; import {filter, take, takeUntil} from 'rxjs/operators'; import {OutletRouter} from './client/router-outlet/outlet-router'; @@ -31,27 +26,29 @@ import {FocusInEventDispatcher} from './client/focus/focus-in-event-dispatcher'; import {FocusMonitor} from './client/focus/focus-monitor'; import {ContextService} from './client/context/context-service'; import {RouterOutletUrlAssigner} from './client/router-outlet/router-outlet-url-assigner'; -import {IS_PLATFORM_HOST} from './platform.model'; +import {APP_IDENTITY, IS_PLATFORM_HOST, ɵAPP_CONFIG} from './platform.model'; import {RelativePathResolver} from './client/router-outlet/relative-path-resolver'; import {ClientRegistry} from './host/message-broker/client.registry'; import {FocusTracker} from './host/focus/focus-tracker'; import {PreferredSizeService} from './client/preferred-size/preferred-size-service'; import {MouseMoveEventDispatcher} from './client/mouse-event/mouse-move-event-dispatcher'; import {MouseUpEventDispatcher} from './client/mouse-event/mouse-up-event-dispatcher'; -import {HostPlatformAppProvider} from './host/host-platform-app-provider'; import {KeyboardEventDispatcher} from './client/keyboard-event/keyboard-event-dispatcher'; import {ManifestService} from './client/manifest-registry/manifest-service'; import {ɵManifestRegistry} from './host/manifest-registry/ɵmanifest-registry'; -import {PlatformManifestService} from './client/manifest-registry/platform-manifest-service'; import {ActivatorInstaller} from './host/activator/activator-installer'; import {BrokerGateway, NullBrokerGateway, ɵBrokerGateway} from './client/messaging/broker-gateway'; import {PlatformState, Runlevel} from './platform-state'; -import {AbstractType, BeanInstanceConstructInstructions, Beans, Type} from '@scion/toolkit/bean-manager'; +import {BeanInstanceConstructInstructions, Beans} from '@scion/toolkit/bean-manager'; import {ɵIntentClient} from './client/messaging/ɵintent-client'; import {ɵMessageClient} from './client/messaging/ɵmessage-client'; import {PlatformStateRef} from './platform-state-ref'; import {ProgressMonitor} from './host/progress-monitor/progress-monitor'; import {ActivatorLoadProgressMonitor, ManifestLoadProgressMonitor} from './host/progress-monitor/progress-monitors'; +import {PlatformTopics} from './ɵmessaging.model'; +import {createHostApplicationConfig} from './host/host-application-config-provider'; +import {HostManifestInterceptor, ɵHostManifestInterceptor} from './host/host-manifest-interceptor'; +import {ApplicationConfig} from './host/application-config'; window.addEventListener('beforeunload', () => MicrofrontendPlatform.destroy(), {once: true}); @@ -115,65 +112,48 @@ export class MicrofrontendPlatform { * Starts the platform in the host application. * * The host application, sometimes also called the container application, provides the top-level integration container for microfrontends. Typically, it is the web - * app which the user loads into his browser and provides the main application shell, defining areas to embed microfrontends. + * application which the user loads into his browser that provides the main application shell, defining areas to embed microfrontends. * - * The platform host loads the manifests of all registered micro applications and starts platform services such as the message broker for client-side messaging. It further may - * wait for activators to signal ready. Typically, the platform is started during bootstrapping the application. In Angular, for example, the platform can be started in an - * app initializer. + * The platform should be started during bootstrapping of the host application. In Angular, for example, the platform is typically started in an app initializer. * - * You can pass the configuration statically, or load it asynchronously using a config loader, e.g., for loading the config over the network. + * In the host, the web applications are registered as micro applications. Registered micro applications can interact with the platform and other micro applications. + * As with micro applications, the host can provide a manifest to contribute behavior. For more information, see {@link MicrofrontendPlatformConfig.host.manifest}. + * If you are integrating the platform in a library, you may want to add behavior to the host's manifest, which you can do with a {@link HostManifestInterceptor}. * - * Note: If the host app wants to interact with either the platform or the micro applications, the host app also has to register itself as a micro application. The host app has - * no extra privileges compared to other micro applications. + * During platform startup, the platform loads the manifests of registered micro applications. Because starting the platform is an asynchronous operation, you should + * wait for the startup Promise to resolve before interacting with the platform. Optionally, you can subscribe to the platform’s startup progress to provide feedback + * to the user about the progress of the platform startup. See {@link MicrofrontendPlatform.startupProgress$} for more information. * - * #### Platform Startup - * During startup, the platform cycles through different {@link Runlevel runlevels} for running initializers, enabling the controlled initialization of platform services. Initializers can specify a - * runlevel in which to execute. The platform enters the state {@link PlatformState.Started} after all initializers have completed. + * In the lifecycle of the platform, it traverses different lifecycle states that you can hook into by registering a callback to {@link MicrofrontendPlatform.whenState}. + * To hook into the startup of the platform, you can register an initializer using {@link Beans.registerInitializer}, optionally passing a runlevel to control when the initializer + * will execute. The platform supports following runlevels: * - * - In runlevel 0, the platform fetches manifests of registered micro applications. - * - In runlevel 1, the platform constructs eager beans. - * - From runlevel 2 and above, messaging is enabled. This is the default runlevel at which initializers execute if not specifying any runlevel. - * - In runlevel 3, the platform installs activator microfrontends. + * - In runlevel `0`, the platform fetches manifests of registered micro applications. + * - In runlevel `1`, the platform constructs eager beans. + * - From runlevel `2` and above, messaging is enabled. This is the default runlevel at which initializers execute if not specifying any runlevel. + * - In runlevel `3`, the platform installs activator microfrontends. See https://scion-microfrontend-platform-developer-guide.vercel.app/#chapter:activator to learn more about activators. * - * @param platformConfig - Platform config declaring the micro applications allowed to interact with the platform. You can pass the configuration statically, or load it - * asynchronously using a config loader, e.g., for loading the config over the network. - * @param hostAppConfig - Config of the micro application running in the host application; only required if interacting with the platform in the host application. - * @return A Promise that resolves when the platform started successfully and activators, if any, signaled ready, or that rejects if the startup fails. + * @param config - Configures the platform and defines the micro applications running in the platform. + * @return A Promise that resolves once platform startup completed. */ - public static startHost(platformConfig: ApplicationConfig[] | PlatformConfig | Type, hostAppConfig?: MicroApplicationConfig): Promise { - return MicrofrontendPlatform.startPlatform(() => { + public static startHost(config: MicrofrontendPlatformConfig): Promise { + return MicrofrontendPlatform.startPlatform(async () => { + await SciRouterOutletElement.define(); MicrofrontendPlatform.installHostStartupProgressMonitor(); - const ɵPlatformBrokerGatewaySymbol = Symbol('INTERNAL_PLATFORM_BROKER_GATEWAY'); - - // Construct the message broker in runlevel 0 to buffer connect requests of micro applications. - Beans.registerInitializer({useFunction: async () => void (Beans.get(MessageBroker)), runlevel: Runlevel.Zero}); - // Fetch manifests in runlevel 0. - Beans.registerInitializer({useClass: ManifestCollector, runlevel: Runlevel.Zero}); - // Install activator microfrontends in runlevel 3, so messaging is enabled and eager beans constructed - Beans.registerInitializer({useClass: ActivatorInstaller, runlevel: Runlevel.Three}); - Beans.registerInitializer(() => SciRouterOutletElement.define()); - - // Install initializer to block startup until connected to the message broker. It rejects if the maximal broker discovery timeout elapses. - Beans.registerInitializer({useFunction: () => Beans.get(BrokerGateway).whenConnected(), runlevel: Runlevel.One}); - Beans.registerInitializer({useFunction: () => Beans.get(ɵPlatformBrokerGatewaySymbol).whenConnected(), runlevel: Runlevel.One}); - // Obtain platform properties and applications before signaling the platform as started to allow synchronous retrieval. - Beans.registerInitializer({ - useFunction: async () => { - if (Beans.get(BrokerGateway) instanceof NullBrokerGateway) { - return; - } - await Beans.get(PlatformPropertyService).whenPropertiesLoaded; - await Beans.opt(ManifestService)?.whenApplicationsLoaded; // bean is not installed when starting the host platform anonymously - }, runlevel: Runlevel.Two, - }); + registerRunlevel0Initializers(); + registerRunlevel1Initializers(); + registerRunlevel2Initializers(); + registerRunlevel3Initializers(); + + Beans.register(APP_IDENTITY, {useValue: config.host?.symbolicName || 'host'}); + Beans.register(MicrofrontendPlatformConfig, {useValue: config}); Beans.register(IS_PLATFORM_HOST, {useValue: true}); - Beans.register(HostPlatformAppProvider); + Beans.register(HostManifestInterceptor, {useClass: ɵHostManifestInterceptor, multi: true}); Beans.register(ClientRegistry); Beans.registerIfAbsent(Logger, {useClass: ConsoleLogger}); Beans.register(PlatformPropertyService, {eager: true}); Beans.registerIfAbsent(HttpClient); - Beans.register(PlatformConfigLoader, createConfigLoaderBeanDescriptor(platformConfig)); Beans.register(ManifestRegistry, {useClass: ɵManifestRegistry, eager: true}); Beans.register(ApplicationRegistry, {eager: true}); Beans.register(ContextService); @@ -181,88 +161,123 @@ export class MicrofrontendPlatform { Beans.register(FocusInEventDispatcher, {eager: true}); Beans.register(MouseMoveEventDispatcher, {eager: true}); Beans.register(MouseUpEventDispatcher, {eager: true}); - Beans.register(PlatformManifestService); Beans.register(MessageBroker, {destroyOrder: Number.MAX_VALUE}); Beans.registerIfAbsent(OutletRouter); Beans.registerIfAbsent(RelativePathResolver); Beans.registerIfAbsent(RouterOutletUrlAssigner); Beans.register(PlatformStateRef, {useValue: MicrofrontendPlatform}); - - Beans.register(ɵPlatformBrokerGatewaySymbol, provideBrokerGateway(PLATFORM_SYMBOLIC_NAME, hostAppConfig && hostAppConfig.messaging)); - Beans.registerIfAbsent(PlatformMessageClient, provideMessageClient(ɵPlatformBrokerGatewaySymbol)); - Beans.registerIfAbsent(PlatformIntentClient, provideIntentClient(ɵPlatformBrokerGatewaySymbol)); - - if (hostAppConfig) { - Beans.register(MicroApplicationConfig, {useValue: hostAppConfig}); - Beans.register(BrokerGateway, provideBrokerGateway(hostAppConfig.symbolicName, hostAppConfig.messaging)); - Beans.registerIfAbsent(MessageClient, provideMessageClient(BrokerGateway)); - Beans.registerIfAbsent(IntentClient, provideIntentClient(BrokerGateway)); - Beans.register(FocusMonitor); - Beans.register(PreferredSizeService); - Beans.register(ManifestService); - Beans.register(KeyboardEventDispatcher, {eager: true}); - } - else { - Beans.registerIfAbsent(MessageClient, {useExisting: PlatformMessageClient}); - Beans.registerIfAbsent(IntentClient, {useExisting: PlatformIntentClient}); - Beans.registerIfAbsent(BrokerGateway, {useExisting: ɵPlatformBrokerGatewaySymbol}); - } + Beans.registerIfAbsent(MessageClient, provideMessageClient()); + Beans.registerIfAbsent(IntentClient, provideIntentClient()); + Beans.register(FocusMonitor); + Beans.register(PreferredSizeService); + Beans.register(ManifestService); + Beans.register(KeyboardEventDispatcher, {eager: true}); + Beans.register(BrokerGateway, provideBrokerGateway({ + connectToHost: true, + messageDeliveryTimeout: config.host?.messageDeliveryTimeout, + })); + + // Register app configs under the symbol `ɵAPP_CONFIG` in the bean manager. + new Array() + .concat(createHostApplicationConfig(config.host)) + .concat(config.applications) + .filter(application => !application.exclude) + .forEach(application => Beans.register(ɵAPP_CONFIG, {useValue: application, multi: true})); }, ); + + /** + * Registers initializers to run in runlevel 0. + */ + function registerRunlevel0Initializers(): void { + // Construct the message broker to buffer connect requests of micro applications. + Beans.registerInitializer({useFunction: async () => void (Beans.get(MessageBroker)), runlevel: Runlevel.Zero}); + // Fetch manifests. + Beans.registerInitializer({useClass: ManifestCollector, runlevel: Runlevel.Zero}); + } + + /** + * Registers initializers to run in runlevel 1. + */ + function registerRunlevel1Initializers(): void { + // Wait until connected to the message broker, or reject if the maximal broker discovery timeout has elapsed. + Beans.registerInitializer({ + useFunction: () => Beans.get(BrokerGateway).whenConnected(), + runlevel: Runlevel.One, + }); + } + + /** + * Registers initializers to run in runlevel 2. + */ + function registerRunlevel2Initializers(): void { + // After messaging is enabled, publish platform properties as retained message. + Beans.registerInitializer({ + useFunction: () => Beans.get(MessageClient).publish(PlatformTopics.PlatformProperties, config.properties || {}, {retain: true}), + runlevel: Runlevel.Two, + }); + // After messaging is enabled, publish registered applications as retained message. + Beans.registerInitializer({ + useFunction: () => Beans.get(MessageClient).publish(PlatformTopics.Applications, Beans.get(ApplicationRegistry).getApplications(), {retain: true}), + runlevel: Runlevel.Two, + }); + // Wait until obtained platform properties so that they can be accessed synchronously by the application via `PlatformPropertyService#properties`. + Beans.registerInitializer({ + useFunction: () => Beans.get(PlatformPropertyService).whenPropertiesLoaded, + runlevel: Runlevel.Two, + }); + // Wait until obtained registered applications so that they can be accessed synchronously by the application via `ManifestService#applications`. + Beans.registerInitializer({ + useFunction: () => Beans.get(ManifestService).whenApplicationsLoaded, + runlevel: Runlevel.Two, + }); + } + + /** + * Registers initializers to run in runlevel 3. + */ + function registerRunlevel3Initializers(): void { + // Install activator microfrontends. + Beans.registerInitializer({useClass: ActivatorInstaller, runlevel: Runlevel.Three}); + } } /** - * Allows a micro application to connect to the platform host. - * - * The platform host checks whether the connecting micro application is a registered micro application. It also checks its origin, - * i.e., that it matches the manifest origin of the registered micro application. This check prevents micro applications from - * connecting to the platform on behalf of other micro applications. + * Connects a micro application to the platform host. * - * When connected to the platform, the micro application can interact with the platform and other micro applications. Typically, the - * micro application connects to the platform host during bootstrapping, that is, before displaying content to the user. In Angular, for - * example, this can be done in an app initializer. + * The platform host checks whether the connecting micro application is qualified to connect, i.e., is registered in the host application under that origin; + * otherwise, the host will reject the connection attempt. Note that the micro application needs to be embedded as a direct or indirect child window of the + * host application window. * - * Note: To establish the connection, the micro application needs to be registered in the host application and embedded as a direct or indirect - * child window of the host application window. + * After the connection with the platform host is established, the micro application can interact with the host and other micro applications. Typically, the + * micro application connects to the platform host during bootstrapping. In Angular, for example, this can be done in an app initializer. * - * #### Platform Startup - * During startup, the platform cycles through different {@link Runlevel runlevels} for running initializers, enabling the controlled initialization of platform services. Initializers can specify a - * runlevel in which to execute. The platform enters the state {@link PlatformState.Started} after all initializers have completed. + * In the lifecycle of the platform, it traverses different lifecycle states that you can hook into by registering a callback to {@link MicrofrontendPlatform.whenState}. * - * - In runlevel 1, the platform constructs eager beans. - * - Runlevel 2 is the default runlevel at which initializers execute if not specifying any runlevel. - * - * @param config - Identity of the micro application to connect. - * @return A Promise that resolves when the platform started successfully, or that rejects if the startup fails. + * @param symbolicName - Specifies the symbolic name of this micro application. The micro application must be registered in the platform host under this symbol. + * @param connectOptions - Controls how to connect to the platform host. + * @return A Promise that resolves once connected to the platform host, or that rejects otherwise. */ - public static connectToHost(config: MicroApplicationConfig): Promise { - return MicrofrontendPlatform.startPlatform(() => { + public static connectToHost(symbolicName: string, connectOptions?: ConnectOptions): Promise { + return MicrofrontendPlatform.startPlatform(async () => { + await SciRouterOutletElement.define(); this.installClientStartupProgressMonitor(); - // Obtain platform properties and applications before signaling the platform as started to allow synchronous retrieval. - Beans.registerInitializer({ - useFunction: async () => { - if (Beans.get(BrokerGateway) instanceof NullBrokerGateway) { - return; - } - await Beans.get(PlatformPropertyService).whenPropertiesLoaded; - await Beans.get(ManifestService).whenApplicationsLoaded; - }, runlevel: Runlevel.Two, - }); - - // Install initializer to block startup until connected to the message broker. It rejects if the maximal broker discovery timeout elapses. - Beans.registerInitializer({useFunction: () => Beans.get(BrokerGateway).whenConnected(), runlevel: Runlevel.One}); - - Beans.registerInitializer(() => SciRouterOutletElement.define()); + registerRunlevel1Initializers(); + registerRunlevel2Initializers(); Beans.register(IS_PLATFORM_HOST, {useValue: false}); - Beans.register(MicroApplicationConfig, {useValue: config}); + Beans.register(APP_IDENTITY, {useValue: symbolicName}); Beans.register(PlatformPropertyService, {eager: true}); Beans.registerIfAbsent(Logger, {useClass: ConsoleLogger}); Beans.registerIfAbsent(HttpClient); - Beans.registerIfAbsent(BrokerGateway, provideBrokerGateway(config.symbolicName, config.messaging)); - Beans.registerIfAbsent(MessageClient, provideMessageClient(BrokerGateway)); - Beans.registerIfAbsent(IntentClient, provideIntentClient(BrokerGateway)); + Beans.register(BrokerGateway, provideBrokerGateway({ + connectToHost: connectOptions?.connect ?? true, + messageDeliveryTimeout: connectOptions?.messageDeliveryTimeout, + brokerDiscoveryTimeout: connectOptions?.brokerDiscoverTimeout, + })); + Beans.registerIfAbsent(MessageClient, provideMessageClient()); + Beans.registerIfAbsent(IntentClient, provideIntentClient()); Beans.registerIfAbsent(OutletRouter); Beans.registerIfAbsent(RelativePathResolver); Beans.registerIfAbsent(RouterOutletUrlAssigner); @@ -277,10 +292,38 @@ export class MicrofrontendPlatform { Beans.register(PlatformStateRef, {useValue: MicrofrontendPlatform}); }, ); + + /** + * Registers initializers to run in runlevel 1. + */ + function registerRunlevel1Initializers(): void { + // Wait until connected to the message broker, or reject if the maximal broker discovery timeout has elapsed. + Beans.registerInitializer({ + useFunction: () => Beans.get(BrokerGateway).whenConnected(), + runlevel: Runlevel.One, + }); + } + + /** + * Registers initializers to run in runlevel 2. + */ + function registerRunlevel2Initializers(): void { + // Wait until obtained platform properties so that they can be accessed synchronously by the application via `PlatformPropertyService#properties`. + Beans.registerInitializer({ + useFunction: () => Beans.get(PlatformPropertyService).whenPropertiesLoaded, + runlevel: Runlevel.Two, + }); + + // Wait until obtained registered applications so that they can be accessed synchronously by the application via `ManifestService#applications`. + Beans.registerInitializer({ + useFunction: () => Beans.get(ManifestService).whenApplicationsLoaded, + runlevel: Runlevel.Two, + }); + } } /** - * Checks if this micro application is connected to the platform host. + * Checks whether this micro application is connected to the platform host. */ public static async isConnectedToHost(): Promise { if (MicrofrontendPlatform.state === PlatformState.Stopped) { @@ -305,18 +348,17 @@ export class MicrofrontendPlatform { } /** @internal */ - public static async startPlatform(startupFn?: () => void): Promise { + public static async startPlatform(startupFn?: () => Promise): Promise { await MicrofrontendPlatform.enterState(PlatformState.Starting); try { - startupFn && startupFn(); - + await startupFn?.(); await Beans.start({eagerBeanConstructRunlevel: Runlevel.One, initializerDefaultRunlevel: Runlevel.Two}); await MicrofrontendPlatform.enterState(PlatformState.Started); return Promise.resolve(); } catch (error) { Beans.destroy(); - return Promise.reject(`[PlatformStartupError] Microfrontend platform failed to start: ${error}`); + return Promise.reject(`[MicrofrontendPlatformStartupError] Microfrontend platform failed to start: ${error}`); } } @@ -370,8 +412,7 @@ export class MicrofrontendPlatform { * and waits for the applications to signal their readiness, which can take some time. * * You can subscribe to this Observable to provide feedback to the user about the progress of the platform startup. - * The Observable reports the progress as a percentage number between `0` and `100`. The Observable completes - * once the platform finished startup. + * The Observable reports the progress as a percentage number. The Observable completes once the platform finished startup. */ public static get startupProgress$(): Observable { return this._startupProgress$; @@ -410,69 +451,35 @@ export class MicrofrontendPlatform { } } -/** - * Creates a {@link PlatformConfigLoader} from the given config. - * @ignore - */ -function createConfigLoaderBeanDescriptor(config: ApplicationConfig[] | PlatformConfig | Type): BeanInstanceConstructInstructions { - if (typeof config === 'function') { - return {useClass: config}; // {PlatformConfigLoader} class - } - else if (Array.isArray(config)) { // array of {ApplicationConfig} objects - return {useValue: new StaticPlatformConfigLoader({apps: config, properties: {}})}; - } - else { // {PlatformConfig} object - return {useValue: new StaticPlatformConfigLoader(config)}; - } -} - /** @ignore */ -function provideBrokerGateway(clientAppName: string, config?: {enabled?: boolean; brokerDiscoverTimeout?: number; deliveryTimeout?: number}): BeanInstanceConstructInstructions { - if (!Defined.orElse(config?.enabled, true)) { +function provideBrokerGateway(config: {connectToHost: boolean; messageDeliveryTimeout?: number; brokerDiscoveryTimeout?: number}): BeanInstanceConstructInstructions { + if (!config.connectToHost) { return {useClass: NullBrokerGateway}; } return { - useFactory: (): BrokerGateway => { - const discoveryTimeout = Defined.orElse(config && config.brokerDiscoverTimeout, 10000); - const deliveryTimeout = Defined.orElse(config && config.deliveryTimeout, 10000); - return new ɵBrokerGateway(clientAppName, {discoveryTimeout, deliveryTimeout}); - }, + useFactory: () => new ɵBrokerGateway({ + brokerDiscoveryTimeout: config.brokerDiscoveryTimeout ?? 10000, + messageDeliveryTimeout: config.messageDeliveryTimeout ?? 10000, + }), eager: true, destroyOrder: Number.MAX_VALUE, }; } /** @ignore */ -function provideMessageClient(brokerGatewayType: AbstractType | symbol): BeanInstanceConstructInstructions { +function provideMessageClient(): BeanInstanceConstructInstructions { return { - useFactory: (): MessageClient => { - const brokerGateway = Beans.get(brokerGatewayType); - return new ɵMessageClient(brokerGateway); - }, + useClass: ɵMessageClient, eager: true, destroyOrder: Number.MAX_VALUE, }; } /** @ignore */ -function provideIntentClient(brokerGatewayType: AbstractType | symbol): BeanInstanceConstructInstructions { +function provideIntentClient(): BeanInstanceConstructInstructions { return { - useFactory: (): IntentClient => { - const brokerGateway = Beans.get(brokerGatewayType); - return new ɵIntentClient(brokerGateway); - }, + useClass: ɵIntentClient, eager: true, destroyOrder: Number.MAX_VALUE, }; } - -/** @ignore */ -class StaticPlatformConfigLoader implements PlatformConfigLoader { - - constructor(private _config: PlatformConfig) { - } - - public load(): Promise { - return Promise.resolve(this._config); - } -} diff --git a/projects/scion/microfrontend-platform/src/lib/operators.ts b/projects/scion/microfrontend-platform/src/lib/operators.ts index a68b92f7..3ef9c9a4 100644 --- a/projects/scion/microfrontend-platform/src/lib/operators.ts +++ b/projects/scion/microfrontend-platform/src/lib/operators.ts @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -import {MonoTypeOperatorFunction, OperatorFunction, pipe, throwError} from 'rxjs'; +import {identity, MonoTypeOperatorFunction, OperatorFunction, pipe, throwError} from 'rxjs'; import {filter, map, timeoutWith} from 'rxjs/operators'; import {MessageEnvelope, MessagingChannel, MessagingTransport} from './ɵmessaging.model'; import {Message, TopicMessage} from './messaging.model'; @@ -78,5 +78,5 @@ export function timeoutIfPresent(timeout: number | undefined): MonoTypeOperat if (timeout) { return timeoutWith(new Date(Date.now() + timeout), throwError(`Timeout of ${timeout}ms elapsed.`)); } - return pipe(); + return identity; } diff --git a/projects/scion/microfrontend-platform/src/lib/platform-property-service.ts b/projects/scion/microfrontend-platform/src/lib/platform-property-service.ts index f8032aa9..57cd7fb3 100644 --- a/projects/scion/microfrontend-platform/src/lib/platform-property-service.ts +++ b/projects/scion/microfrontend-platform/src/lib/platform-property-service.ts @@ -15,9 +15,10 @@ import {Dictionary, Maps} from '@scion/toolkit/util'; import {Subject} from 'rxjs'; import {mapToBody} from './messaging.model'; import {Beans, PreDestroy} from '@scion/toolkit/bean-manager'; +import {BrokerGateway, NullBrokerGateway} from './client/messaging/broker-gateway'; /** - * Allows looking up properties defined on the platform host. + * Allows looking up properties defined in the platform host. * * @category Platform */ @@ -28,13 +29,15 @@ export class PlatformPropertyService implements PreDestroy { /** * Promise that resolves when loaded the properties from the host. + * If messaging is disabled, the Promise resolves immediately. * * @internal */ public whenPropertiesLoaded: Promise; constructor() { - this.whenPropertiesLoaded = this.loadProperties(); + const messagingDisabled = Beans.get(BrokerGateway) instanceof NullBrokerGateway; + this.whenPropertiesLoaded = messagingDisabled ? Promise.resolve() : this.requestProperties(); } /** @@ -66,7 +69,7 @@ export class PlatformPropertyService implements PreDestroy { return this._properties; } - private async loadProperties(): Promise { + private async requestProperties(): Promise { this._properties = await Beans.get(MessageClient).observe$(PlatformTopics.PlatformProperties) .pipe( mapToBody(), diff --git a/projects/scion/microfrontend-platform/src/lib/platform.model.ts b/projects/scion/microfrontend-platform/src/lib/platform.model.ts index d79697ae..d60d7be2 100644 --- a/projects/scion/microfrontend-platform/src/lib/platform.model.ts +++ b/projects/scion/microfrontend-platform/src/lib/platform.model.ts @@ -16,7 +16,7 @@ * * @category Platform */ -export interface ApplicationManifest { +export interface Manifest { /** * The name of the application, e.g. displayed in the DevTools. */ @@ -78,17 +78,17 @@ export interface Application { */ manifestUrl: string; /** - * Maximum time (in milliseconds) that the host waits for this application to fetch its manifest. + * Maximum time (in milliseconds) that the host waits until the manifest for this application is loaded. * * This is the effective timeout, i.e, either the application-specific timeout as defined in {@link ApplicationConfig.manifestLoadTimeout}, - * or the global timeout as defined in {@link PlatformConfig.manifestLoadTimeout}, otherwise `undefined`. + * or the global timeout as defined in {@link MicrofrontendPlatformConfig.manifestLoadTimeout}, otherwise `undefined`. */ manifestLoadTimeout?: number; /** * Maximum time (in milliseconds) that the host waits for this application to signal readiness. * * This is the effective timeout, i.e, either the application-specific timeout as defined in {@link ApplicationConfig.activatorLoadTimeout}, - * or the global timeout as defined in {@link PlatformConfig.activatorLoadTimeout}, otherwise `undefined`. + * or the global timeout as defined in {@link MicrofrontendPlatformConfig.activatorLoadTimeout}, otherwise `undefined`. */ activatorLoadTimeout?: number; /** @@ -379,3 +379,11 @@ export interface ParamDefinition { */ [property: string]: any; } + +/** @internal */ +export const ɵAPP_CONFIG = Symbol('ɵAPP_CONFIG'); + +/** + * Symbol to get the application's symbolic name from the bean manager. + */ +export const APP_IDENTITY = Symbol('APP_IDENTITY'); diff --git a/projects/scion/microfrontend-platform/src/lib/spec.util.spec.ts b/projects/scion/microfrontend-platform/src/lib/spec.util.spec.ts index 3d3bba95..789d962b 100644 --- a/projects/scion/microfrontend-platform/src/lib/spec.util.spec.ts +++ b/projects/scion/microfrontend-platform/src/lib/spec.util.spec.ts @@ -8,7 +8,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -import {ApplicationManifest} from './platform.model'; +import {Manifest} from './platform.model'; import {Arrays} from '@scion/toolkit/util'; import {ObserveCaptor} from '@scion/toolkit/testing'; import {ConsoleLogger, Logger} from './logger'; @@ -75,7 +75,7 @@ export interface PromiseMatcher { /*** * Serves the given manifest and returns the URL where the manifest is served. */ -export function serveManifest(manifest: Partial): string { +export function serveManifest(manifest: Partial): string { return URL.createObjectURL(new Blob([JSON.stringify(manifest)], {type: 'application/json'})); } diff --git a/projects/scion/microfrontend-platform/tsconfig.lib.prod.typedoc.json b/projects/scion/microfrontend-platform/tsconfig.lib.prod.typedoc.json index 3a7145d6..979b4ef4 100644 --- a/projects/scion/microfrontend-platform/tsconfig.lib.prod.typedoc.json +++ b/projects/scion/microfrontend-platform/tsconfig.lib.prod.typedoc.json @@ -13,6 +13,8 @@ "exclude": "**/ɵ*.ts", "toc": [ "MicrofrontendPlatform", + "Manifest", + "MicrofrontendPlatformConfig", "MessageClient", "IntentClient", "OutletRouter", @@ -24,14 +26,9 @@ "MessageInterceptor", "IntentInterceptor", "HostPlatformState", - "PlatformConfigLoader", "PlatformPropertyService", "Beans", "Activator", - "ApplicationManifest", - "ApplicationConfig", - "PlatformConfigLoader", - "PlatformConfig" ], "categoryOrder": [ "Platform",