From c29ed517e374865380d0d95c8936f65d79a9622d Mon Sep 17 00:00:00 2001 From: enrico Date: Tue, 20 Feb 2024 00:03:48 +0100 Subject: [PATCH 1/8] manage instances --- .../src/single-spa-angular.ts | 42 ++++++++++++------- libs/single-spa-angular/src/types.ts | 6 +++ 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/libs/single-spa-angular/src/single-spa-angular.ts b/libs/single-spa-angular/src/single-spa-angular.ts index fa1ac01..0a3e81d 100644 --- a/libs/single-spa-angular/src/single-spa-angular.ts +++ b/libs/single-spa-angular/src/single-spa-angular.ts @@ -4,7 +4,7 @@ import { LifeCycles } from 'single-spa'; import { getContainerElementAndSetTemplate } from 'single-spa-angular/internals'; import { SingleSpaPlatformLocation } from './extra-providers'; -import { SingleSpaAngularOptions, BootstrappedSingleSpaAngularOptions } from './types'; +import { SingleSpaAngularOptions, BootstrappedSingleSpaAngularOptions, Instance } from './types'; const defaultOptions = { // Required options that will be set by the library consumer. @@ -61,6 +61,11 @@ export function singleSpaAngular(userOptions: SingleSpaAngularOptions): Li } async function bootstrap(options: BootstrappedSingleSpaAngularOptions, props: any): Promise { + const instance: Instance = { + bootstrappedNgModuleRefOrAppRef: null + }; + options.instances[props.name || props.appName] = instance; + // Angular provides an opportunity to develop `zone-less` application, where developers // have to trigger change detection manually. // See https://angular.io/guide/zone#noopzone @@ -68,21 +73,24 @@ async function bootstrap(options: BootstrappedSingleSpaAngularOptions, props: an return; } + // Set NgZone on instance. + instance.NgZone = options.NgZone; + // In order for multiple Angular apps to work concurrently on a page, they each need a unique identifier. - options.zoneIdentifier = `single-spa-angular:${props.name || props.appName}`; + instance.zoneIdentifier = `single-spa-angular:${props.name || props.appName}`; // This is a hack, since NgZone doesn't allow you to configure the property that identifies your zone. // See https://github.com/PlaceMe-SAS/single-spa-angular-cli/issues/33, // https://github.com/single-spa/single-spa-angular/issues/47, // https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L144, // and https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L257 - options.NgZone.isInAngularZone = () => { + instance.NgZone.isInAngularZone = () => { // @ts-ignore return window.Zone.current._properties[options.zoneIdentifier] === true; }; - options.routingEventListener = () => { - options.bootstrappedNgZone!.run(() => { + instance.routingEventListener = () => { + instance.bootstrappedNgZone!.run(() => { // See https://github.com/single-spa/single-spa-angular/issues/86 // Zone is unaware of the single-spa navigation change and so Angular change detection doesn't work // unless we tell Zone that something happened @@ -131,9 +139,11 @@ async function mount( const bootstrappedOptions = options as BootstrappedSingleSpaAngularOptions; + const instance = bootstrappedOptions.instances[props.name || props.appName]; + if (ngZoneEnabled) { const ngZone: NgZone = ngModuleRefOrAppRef.injector.get(options.NgZone); - const zoneIdentifier: string = bootstrappedOptions.zoneIdentifier!; + const zoneIdentifier: string = instance.zoneIdentifier!; // `NgZone` can be enabled but routing may not be used thus `getSingleSpaExtraProviders()` // function was not called. @@ -141,23 +151,25 @@ async function mount( skipLocationChangeOnNonImperativeRoutingTriggers(ngModuleRefOrAppRef, options); } - bootstrappedOptions.bootstrappedNgZone = ngZone; - (bootstrappedOptions.bootstrappedNgZone as any)._inner._properties[zoneIdentifier] = true; - window.addEventListener('single-spa:routing-event', bootstrappedOptions.routingEventListener!); + instance.bootstrappedNgZone = ngZone; + (instance.bootstrappedNgZone as any)._inner._properties[zoneIdentifier] = true; + window.addEventListener('single-spa:routing-event', instance.routingEventListener!); } - bootstrappedOptions.bootstrappedNgModuleRefOrAppRef = ngModuleRefOrAppRef; + instance.bootstrappedNgModuleRefOrAppRef = ngModuleRefOrAppRef; return ngModuleRefOrAppRef; } -function unmount(options: BootstrappedSingleSpaAngularOptions): Promise { +function unmount(options: BootstrappedSingleSpaAngularOptions, props: any): Promise { + const instance: Instance = options.instances[props.name || props.appName]; + return Promise.resolve().then(() => { - if (options.routingEventListener) { - window.removeEventListener('single-spa:routing-event', options.routingEventListener); + if (instance.routingEventListener) { + window.removeEventListener('single-spa:routing-event', instance.routingEventListener); } - options.bootstrappedNgModuleRefOrAppRef!.destroy(); - options.bootstrappedNgModuleRefOrAppRef = null; + instance.bootstrappedNgModuleRefOrAppRef!.destroy(); + instance.bootstrappedNgModuleRefOrAppRef = null; }); } diff --git a/libs/single-spa-angular/src/types.ts b/libs/single-spa-angular/src/types.ts index 6e06dd5..114bfee 100644 --- a/libs/single-spa-angular/src/types.ts +++ b/libs/single-spa-angular/src/types.ts @@ -15,7 +15,13 @@ export interface SingleSpaAngularOptions> } export interface BootstrappedSingleSpaAngularOptions extends SingleSpaAngularOptions { + instances: { [name: string]: Instance }; +} + +export interface Instance { bootstrappedNgModuleRefOrAppRef: NgModuleRef | ApplicationRef | null; + // This is a reference to the `NgZone` class that was used to bootstrap the application. + NgZone?: typeof NgZone | 'noop'; // All below properties can be optional in case of // `SingleSpaAngularOpts.NgZone` is a `noop` string and not an `NgZone` class. bootstrappedNgZone?: NgZone; From 327039a0ef0097d3c9e9e7943b0ec3d5a9fb7272 Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Sun, 31 Mar 2024 00:22:46 +0100 Subject: [PATCH 2/8] add: manage template of instances of same app --- libs/single-spa-angular/internals/src/dom.ts | 5 ++++- .../single-spa-angular/internals/src/types.ts | 2 +- .../src/single-spa-angular.ts | 19 ++++++++++--------- libs/single-spa-angular/src/types.ts | 2 -- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/libs/single-spa-angular/internals/src/dom.ts b/libs/single-spa-angular/internals/src/dom.ts index 2d7e220..77b49f4 100644 --- a/libs/single-spa-angular/internals/src/dom.ts +++ b/libs/single-spa-angular/internals/src/dom.ts @@ -18,8 +18,11 @@ export function getContainerElementAndSetTemplate HTMLElement; export interface BaseSingleSpaAngularOptions { - template: string; + template: string | ((props: AppProps) => string); domElementGetter?: DomElementGetter; bootstrapFunction(props: AppProps): Promise | ApplicationRef>; } diff --git a/libs/single-spa-angular/src/single-spa-angular.ts b/libs/single-spa-angular/src/single-spa-angular.ts index 0a3e81d..46cfa06 100644 --- a/libs/single-spa-angular/src/single-spa-angular.ts +++ b/libs/single-spa-angular/src/single-spa-angular.ts @@ -15,7 +15,7 @@ const defaultOptions = { Router: undefined, domElementGetter: undefined, // only optional if you provide a domElementGetter as a custom prop updateFunction: () => Promise.resolve(), - bootstrappedNgModuleRefOrAppRef: null, + instances: {}, }; // This will be provided through Terser global definitions by Angular CLI. This will @@ -38,8 +38,8 @@ export function singleSpaAngular(userOptions: SingleSpaAngularOptions): Li throw Error('single-spa-angular must be passed an options.bootstrapFunction'); } - if (NG_DEV_MODE && typeof options.template !== 'string') { - throw Error('single-spa-angular must be passed options.template string'); + if (NG_DEV_MODE && typeof options.template !== 'string' && typeof options.template !== 'function') { + throw Error('single-spa-angular must be passed an options.template string or function'); } if (NG_DEV_MODE && !options.NgZone) { @@ -62,7 +62,7 @@ export function singleSpaAngular(userOptions: SingleSpaAngularOptions): Li async function bootstrap(options: BootstrappedSingleSpaAngularOptions, props: any): Promise { const instance: Instance = { - bootstrappedNgModuleRefOrAppRef: null + bootstrappedNgModuleRefOrAppRef: null, }; options.instances[props.name || props.appName] = instance; @@ -73,9 +73,6 @@ async function bootstrap(options: BootstrappedSingleSpaAngularOptions, props: an return; } - // Set NgZone on instance. - instance.NgZone = options.NgZone; - // In order for multiple Angular apps to work concurrently on a page, they each need a unique identifier. instance.zoneIdentifier = `single-spa-angular:${props.name || props.appName}`; @@ -84,9 +81,10 @@ async function bootstrap(options: BootstrappedSingleSpaAngularOptions, props: an // https://github.com/single-spa/single-spa-angular/issues/47, // https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L144, // and https://github.com/angular/angular/blob/a14dc2d7a4821a19f20a9547053a5734798f541e/packages/core/src/zone/ng_zone.ts#L257 - instance.NgZone.isInAngularZone = () => { + options.NgZone.isInAngularZone = () => { // @ts-ignore - return window.Zone.current._properties[options.zoneIdentifier] === true; + // Check zone for any instance. + return Object.values(options.instances).some(instance => window.Zone.current._properties[instance.zoneIdentifier] === true); }; instance.routingEventListener = () => { @@ -170,6 +168,9 @@ function unmount(options: BootstrappedSingleSpaAngularOptions, props: any): Prom instance.bootstrappedNgModuleRefOrAppRef!.destroy(); instance.bootstrappedNgModuleRefOrAppRef = null; + + // Delete instance from array of instances. + delete options.instances[props.name || props.appName]; }); } diff --git a/libs/single-spa-angular/src/types.ts b/libs/single-spa-angular/src/types.ts index 114bfee..2a859cb 100644 --- a/libs/single-spa-angular/src/types.ts +++ b/libs/single-spa-angular/src/types.ts @@ -20,8 +20,6 @@ export interface BootstrappedSingleSpaAngularOptions extends SingleSpaAngularOpt export interface Instance { bootstrappedNgModuleRefOrAppRef: NgModuleRef | ApplicationRef | null; - // This is a reference to the `NgZone` class that was used to bootstrap the application. - NgZone?: typeof NgZone | 'noop'; // All below properties can be optional in case of // `SingleSpaAngularOpts.NgZone` is a `noop` string and not an `NgZone` class. bootstrappedNgZone?: NgZone; From 3632c2d8d7f001660f477cdaf0c106fc7ed7cea3 Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Thu, 6 Jun 2024 22:47:11 +0200 Subject: [PATCH 3/8] fix: don't delete instnace on unmount --- libs/single-spa-angular/src/single-spa-angular.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libs/single-spa-angular/src/single-spa-angular.ts b/libs/single-spa-angular/src/single-spa-angular.ts index 1c584be..cc6a4a2 100644 --- a/libs/single-spa-angular/src/single-spa-angular.ts +++ b/libs/single-spa-angular/src/single-spa-angular.ts @@ -38,7 +38,11 @@ export function singleSpaAngular(userOptions: SingleSpaAngularOptions): Li throw Error('single-spa-angular must be passed an options.bootstrapFunction'); } - if (NG_DEV_MODE && typeof options.template !== 'string' && typeof options.template !== 'function') { + if ( + NG_DEV_MODE && + typeof options.template !== 'string' && + typeof options.template !== 'function' + ) { throw Error('single-spa-angular must be passed an options.template string or function'); } @@ -162,7 +166,7 @@ function unmount(options: BootstrappedSingleSpaAngularOptions, props: any): Prom instance.bootstrappedNgModuleRefOrAppRef = null; // Delete instance from array of instances. - delete options.instances[props.name || props.appName]; + // delete options.instances[props.name || props.appName]; }); } From 33b92a64f3eafea92d8ea7d9a6b874e6e3e64baf Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Fri, 7 Jun 2024 23:54:41 +0200 Subject: [PATCH 4/8] add: test case for multiple-parcels-same-config --- .../multiple-parcels-same-config.spec.js | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cypress/integration/multiple-parcels-same-config.spec.js diff --git a/cypress/integration/multiple-parcels-same-config.spec.js b/cypress/integration/multiple-parcels-same-config.spec.js new file mode 100644 index 0000000..f9d0f50 --- /dev/null +++ b/cypress/integration/multiple-parcels-same-config.spec.js @@ -0,0 +1,50 @@ +/// + +Cypress.Screenshot.defaults({ + screenshotOnRunFailure: false, +}); + +describe('https://github.com/single-spa/single-spa-angular/issues/234', () => { + it('should render the same Angular parcel twice', () => { + cy.visit('/multiple-parcels-same-config') + // Mount the first parcel. + .get('button.mount1') + .click() + // The `wait` command is used because Cypress on the CI level is slower than locally, + // basically it clicks the ` + + + + +
+ parcel 1 +
+
+ + parcel 2 +
+
+
diff --git a/apps/multiple-parcels-same-config/parent/src/app/app.component.ts b/apps/multiple-parcels-same-config/parent/src/app/app.component.ts new file mode 100644 index 0000000..13305cf --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/app/app.component.ts @@ -0,0 +1,47 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { mountRootParcel } from 'single-spa'; + +@Component({ + selector: 'multiple-parcels-same-config', + templateUrl: './app.component.html' +}) +export class AppComponent { + constructor() {} + + @ViewChild('parcelContainer1') parcelContainer1!: ElementRef; + @ViewChild('parcelContainer2') parcelContainer2!: ElementRef; + + mountRootParcel = mountRootParcel; + + props1 = { id: 1 }; + props2 = { id: 2 }; + + parcel1: any; + parcel2: any; + + config() { + return (window as any).System.import('multiple-parcels-same-config-child'); + } + + mount1() { + this.parcel1 = this.mountRootParcel(this.config, { + domElement: this.parcelContainer1.nativeElement, + ...this.props1, + }); + } + + mount2() { + this.parcel2 = this.mountRootParcel(this.config, { + domElement: this.parcelContainer2.nativeElement, + ...this.props2, + }); + } + + unmount1() { + this.parcel1.unmount(); + } + + unmount2() { + this.parcel2.unmount(); + } +} diff --git a/apps/multiple-parcels-same-config/parent/src/app/app.module.ts b/apps/multiple-parcels-same-config/parent/src/app/app.module.ts new file mode 100644 index 0000000..a742cd1 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/app/app.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { APP_BASE_HREF } from '@angular/common'; + +import { AppComponent } from './app.component'; +import { AppRoutingModule } from './app-routing.module'; +import { EmptyRouteComponent } from './empty-route/empty-route.component'; + +@NgModule({ + imports: [BrowserAnimationsModule, AppRoutingModule], + declarations: [AppComponent, EmptyRouteComponent], + bootstrap: [AppComponent], + providers: [], +}) +export class AppModule {} diff --git a/apps/multiple-parcels-same-config/parent/src/app/empty-route/empty-route.component.ts b/apps/multiple-parcels-same-config/parent/src/app/empty-route/empty-route.component.ts new file mode 100755 index 0000000..b838039 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/app/empty-route/empty-route.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app1-empty-route', + template: '', +}) +export class EmptyRouteComponent {} diff --git a/apps/multiple-parcels-same-config/parent/src/assets/.gitkeep b/apps/multiple-parcels-same-config/parent/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/multiple-parcels-same-config/parent/src/environments/environment.prod.ts b/apps/multiple-parcels-same-config/parent/src/environments/environment.prod.ts new file mode 100644 index 0000000..c966979 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true, +}; diff --git a/apps/multiple-parcels-same-config/parent/src/environments/environment.ts b/apps/multiple-parcels-same-config/parent/src/environments/environment.ts new file mode 100644 index 0000000..a20cfe5 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/environments/environment.ts @@ -0,0 +1,3 @@ +export const environment = { + production: false, +}; diff --git a/apps/multiple-parcels-same-config/parent/src/favicon.ico b/apps/multiple-parcels-same-config/parent/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..997406ad22c29aae95893fb3d666c30258a09537 GIT binary patch literal 948 zcmV;l155mgP)CBYU7IjCFmI-B}4sMJt3^s9NVg!P0 z6hDQy(L`XWMkB@zOLgN$4KYz;j0zZxq9KKdpZE#5@k0crP^5f9KO};h)ZDQ%ybhht z%t9#h|nu0K(bJ ztIkhEr!*UyrZWQ1k2+YkGqDi8Z<|mIN&$kzpKl{cNP=OQzXHz>vn+c)F)zO|Bou>E z2|-d_=qY#Y+yOu1a}XI?cU}%04)zz%anD(XZC{#~WreV!a$7k2Ug`?&CUEc0EtrkZ zL49MB)h!_K{H(*l_93D5tO0;BUnvYlo+;yss%n^&qjt6fZOa+}+FDO(~2>G z2dx@=JZ?DHP^;b7*Y1as5^uphBsh*s*z&MBd?e@I>-9kU>63PjP&^#5YTOb&x^6Cf z?674rmSHB5Fk!{Gv7rv!?qX#ei_L(XtwVqLX3L}$MI|kJ*w(rhx~tc&L&xP#?cQow zX_|gx$wMr3pRZIIr_;;O|8fAjd;1`nOeu5K(pCu7>^3E&D2OBBq?sYa(%S?GwG&_0-s%_v$L@R!5H_fc)lOb9ZoOO#p`Nn`KU z3LTTBtjwo`7(HA6 z7gmO$yTR!5L>Bsg!X8616{JUngg_@&85%>W=mChTR;x4`P=?PJ~oPuy5 zU-L`C@_!34D21{fD~Y8NVnR3t;aqZI3fIhmgmx}$oc-dKDC6Ap$Gy>a!`A*x2L1v0 WcZ@i?LyX}70000 + + + + AngularMultipleParcelsSameConfigParent + + + + + + + + diff --git a/apps/multiple-parcels-same-config/parent/src/main.single-spa.ts b/apps/multiple-parcels-same-config/parent/src/main.single-spa.ts new file mode 100644 index 0000000..39507b4 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/main.single-spa.ts @@ -0,0 +1,29 @@ +import { NgZone } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { NavigationStart, Router } from '@angular/router'; +import { singleSpaAngular, getSingleSpaExtraProviders, enableProdMode } from 'single-spa-angular'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; +import { singleSpaPropsSubject } from './single-spa/single-spa-props'; + +if (environment.production) { + enableProdMode(); +} + +const lifecycles = singleSpaAngular({ + bootstrapFunction: singleSpaProps => { + singleSpaPropsSubject.next(singleSpaProps); + return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule( + AppModule, + ); + }, + template: '', + NgZone, + Router, + NavigationStart, +}); + +export const bootstrap = lifecycles.bootstrap; +export const mount = lifecycles.mount; +export const unmount = lifecycles.unmount; diff --git a/apps/multiple-parcels-same-config/parent/src/single-spa/asset-url.ts b/apps/multiple-parcels-same-config/parent/src/single-spa/asset-url.ts new file mode 100644 index 0000000..d8cb3be --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/single-spa/asset-url.ts @@ -0,0 +1,12 @@ +// In single-spa, the assets need to be loaded from a dynamic location, +// instead of hard coded to `/assets`. We use webpack public path for this. +// See https://webpack.js.org/guides/public-path/#root + +export function assetUrl(url: string): string { + // @ts-ignore + const publicPath = __webpack_public_path__; + const publicPathSuffix = publicPath.endsWith('/') ? '' : '/'; + const urlPrefix = url.startsWith('/') ? '' : '/'; + + return `${publicPath}${publicPathSuffix}assets${urlPrefix}${url}`; +} diff --git a/apps/multiple-parcels-same-config/parent/src/single-spa/single-spa-props.ts b/apps/multiple-parcels-same-config/parent/src/single-spa/single-spa-props.ts new file mode 100644 index 0000000..fc133a2 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/single-spa/single-spa-props.ts @@ -0,0 +1,8 @@ +import { ReplaySubject } from 'rxjs'; +import { AppProps } from 'single-spa'; + +export const singleSpaPropsSubject = new ReplaySubject(1); + +// Add any custom single-spa props you have to this type def +// https://single-spa.js.org/docs/building-applications.html#custom-props +export type SingleSpaProps = AppProps & Record; diff --git a/apps/multiple-parcels-same-config/parent/src/styles.scss b/apps/multiple-parcels-same-config/parent/src/styles.scss new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/src/styles.scss @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/apps/multiple-parcels-same-config/parent/tsconfig.app.json b/apps/multiple-parcels-same-config/parent/tsconfig.app.json new file mode 100644 index 0000000..79f5c90 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/tsconfig.app.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "files": ["src/main.single-spa.ts"] +} diff --git a/apps/multiple-parcels-same-config/parent/tsconfig.json b/apps/multiple-parcels-same-config/parent/tsconfig.json new file mode 100644 index 0000000..11a1eaf --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.json", + "references": [ + { + "path": "./tsconfig.app.json" + } + ], + "compilerOptions": { + "target": "es2020" + } +} diff --git a/apps/multiple-parcels-same-config/parent/webpack.config.ts b/apps/multiple-parcels-same-config/parent/webpack.config.ts new file mode 100644 index 0000000..4411334 --- /dev/null +++ b/apps/multiple-parcels-same-config/parent/webpack.config.ts @@ -0,0 +1 @@ +export { default } from '../../../libs/single-spa-angular/webpack'; diff --git a/apps/navbar/src/app/components/primary-nav/primary-nav.component.ts b/apps/navbar/src/app/components/primary-nav/primary-nav.component.ts index d3cccdf..14a31bf 100644 --- a/apps/navbar/src/app/components/primary-nav/primary-nav.component.ts +++ b/apps/navbar/src/app/components/primary-nav/primary-nav.component.ts @@ -32,6 +32,10 @@ export class PrimaryNavComponent { label: 'Angular standalone', url: '/standalone', }, + { + label: "Multiple parcels from same config", + url: "/multiple-parcels-same-config" + } ]; constructor(private router: Router) {} diff --git a/apps/root-config/src/index.ejs b/apps/root-config/src/index.ejs index c1b4e4b..2155dcc 100644 --- a/apps/root-config/src/index.ejs +++ b/apps/root-config/src/index.ejs @@ -34,6 +34,8 @@ "elements": "http://localhost:4000/main.js", "parcel": "http://localhost:4400/main.js", "standalone": "http://localhost:4500/main.js", + "multiple-parcels-same-config": "http://localhost:4600/main.js", + "multiple-parcels-same-config-child": "http://localhost:4700/main.js", "rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs.min.js", "rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs-operators.min.js" } diff --git a/apps/root-config/src/main.js b/apps/root-config/src/main.js index 15ac2d0..299fae7 100644 --- a/apps/root-config/src/main.js +++ b/apps/root-config/src/main.js @@ -41,5 +41,11 @@ System.import('single-spa').then(({ registerApplication, start }) => { activeWhen: location => location.pathname.startsWith('/standalone'), }); + registerApplication({ + name: 'multiple-parcels-same-config', + app: () => System.import('multiple-parcels-same-config'), + activeWhen: location => location.pathname.startsWith('/multiple-parcels-same-config'), + }); + start(); }); diff --git a/package.json b/package.json index 4aae638..c81e2fe 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "serve:app:elements": "yarn nx serve elements --disable-host-check --live-reload false --port 4000", "serve:app:parcel": "yarn nx serve parcel --disable-host-check --live-reload false --port 4400", "serve:app:standalone": "yarn nx serve standalone --disable-host-check --live-reload false --port 4500", + "serve:app:multiple-parcels-same-config": "yarn nx serve multiple-parcels-same-config --disable-host-check --live-reload false --port 4600 & yarn nx serve multiple-parcels-same-config-child --disable-host-check --live-reload false --port 4700", "serve:app:root-config": "yarn webpack-dev-server --config apps/root-config/webpack.config.js", "serve:all": "concurrently 'yarn:serve:app:*'", "// - INTEGRATION BUILDS": "Build apps that are required for E2E testing #requires yarn install:integration", @@ -60,6 +61,7 @@ "build:app:elements": "nx build elements", "build:app:parcel": "nx build parcel", "build:app:standalone": "nx build standalone", + "build:app:multiple-parcels-same-config": "nx build multiple-parcels-same-config && nx build multiple-parcels-same-config-child", "build:app:root-config": "yarn webpack --mode production --config apps/root-config/webpack.config.js", "build:all": "yarn build:app:root-config && nx run-many --target build --projects=shop,chat,navbar,noop-zone,elements,parcel,standalone --parallel --configuration production", "// - APPS": "Serve apps that are required for E2E testing #requires yarn build:integration", From 969a79060826c047df93edd9cbacd5a4456927e7 Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Sun, 9 Jun 2024 15:52:34 +0200 Subject: [PATCH 6/8] chore: add comment on unmount --- libs/single-spa-angular/src/single-spa-angular.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/single-spa-angular/src/single-spa-angular.ts b/libs/single-spa-angular/src/single-spa-angular.ts index cc6a4a2..9b1beec 100644 --- a/libs/single-spa-angular/src/single-spa-angular.ts +++ b/libs/single-spa-angular/src/single-spa-angular.ts @@ -165,6 +165,11 @@ function unmount(options: BootstrappedSingleSpaAngularOptions, props: any): Prom instance.bootstrappedNgModuleRefOrAppRef!.destroy(); instance.bootstrappedNgModuleRefOrAppRef = null; + /** + * Don't delete the instance from the array of instances, + * because instance is created in bootstrap and not in mount. + * If we delete it here, then the instance can't be mounted again. + */ // Delete instance from array of instances. // delete options.instances[props.name || props.appName]; }); From 732a33869cc8acc34f2d71f90633952dea4dedee Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Sun, 9 Jun 2024 22:52:21 +0200 Subject: [PATCH 7/8] add: start script for multiple-parcels-same-instance --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c81e2fe..3145fd1 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "start:app:elements": "yarn serve dist/apps/elements -s -l 4000 --cors", "start:app:parcel": "yarn serve dist/apps/parcel -s -l 4400 --cors", "start:app:standalone": "yarn serve dist/apps/standalone -s -l 4500 --cors", + "start:app:multiple-parcels-same-config": "yarn serve dist/apps/multiple-parcels-same-config-parent -s -l 4600 --cors & yarn serve dist/apps/multiple-parcels-same-config-child -s -l 4700 --cors", "start:app:root-config": "yarn serve dist/apps/root-config -s -l 8080 --cors", "// - E2E": "E2E testing", "cy:open": "cypress open", From 45aafd1914818a9a9fd66fd8fe6d81c323e7a70a Mon Sep 17 00:00:00 2001 From: Enrico Lobianco Date: Sun, 9 Jun 2024 22:55:48 +0200 Subject: [PATCH 8/8] add: check before unmount on integration app --- .../parent/src/app/app.component.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/multiple-parcels-same-config/parent/src/app/app.component.ts b/apps/multiple-parcels-same-config/parent/src/app/app.component.ts index 13305cf..dcab473 100644 --- a/apps/multiple-parcels-same-config/parent/src/app/app.component.ts +++ b/apps/multiple-parcels-same-config/parent/src/app/app.component.ts @@ -3,7 +3,7 @@ import { mountRootParcel } from 'single-spa'; @Component({ selector: 'multiple-parcels-same-config', - templateUrl: './app.component.html' + templateUrl: './app.component.html', }) export class AppComponent { constructor() {} @@ -38,10 +38,14 @@ export class AppComponent { } unmount1() { - this.parcel1.unmount(); + if (this.parcel1) { + this.parcel1.unmount(); + } } unmount2() { - this.parcel2.unmount(); + if (this.parcel2) { + this.parcel2.unmount(); + } } }