From 0417a1b95cd1319485c6985281a1f31f3a29d6b8 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 9 Dec 2020 00:04:15 +0200 Subject: [PATCH] fix(upgrade): fix HMR by destroying the AngularJS app when `PlatformRef` is destroyed Previously, Hot Module Replacement (HMR) in a hybrid app would throw an error due to trying to bootstrap the AngularJS app on the same element twice. This commit fixes HMR for hybrid apps by ensuring the AngularJS app is when the Angular `PlatformRef` is [destroyed][1] in the [`module.hot.dispose()` callback][2]. [1]: https://github.com/angular/angular-cli/blob/d3afdcc1b3e40736dab3ca7ae81c462747eb0ac9/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L75 [2]: https://github.com/angular/angular-cli/blob/d3afdcc1b3e40736dab3ca7ae81c462747eb0ac9/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L31 Fixes #39935 --- packages/upgrade/src/common/src/constants.ts | 1 + packages/upgrade/src/common/src/util.ts | 15 +++++++++++++-- .../upgrade/src/dynamic/src/upgrade_adapter.ts | 8 +++++++- .../upgrade/static/src/downgrade_module.ts | 10 ++++++++-- packages/upgrade/static/src/upgrade_module.ts | 18 +++++++++++++++--- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/upgrade/src/common/src/constants.ts b/packages/upgrade/src/common/src/constants.ts index 7c9c5d0c8f766..640d23adf0ea0 100644 --- a/packages/upgrade/src/common/src/constants.ts +++ b/packages/upgrade/src/common/src/constants.ts @@ -15,6 +15,7 @@ export const $INJECTOR = '$injector'; export const $INTERVAL = '$interval'; export const $PARSE = '$parse'; export const $PROVIDE = '$provide'; +export const $ROOT_ELEMENT = '$rootElement'; export const $ROOT_SCOPE = '$rootScope'; export const $SCOPE = '$scope'; export const $TEMPLATE_CACHE = '$templateCache'; diff --git a/packages/upgrade/src/common/src/util.ts b/packages/upgrade/src/common/src/util.ts index 2c39d79eac6c0..0edc5c72f996d 100644 --- a/packages/upgrade/src/common/src/util.ts +++ b/packages/upgrade/src/common/src/util.ts @@ -8,8 +8,8 @@ import {Injector, Type} from '@angular/core'; -import {element as angularElement, IInjectorService, INgModelController} from './angular1'; -import {DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants'; +import {element as angularElement, IAugmentedJQuery, IInjectorService, INgModelController, IRootScopeService} from './angular1'; +import {$ROOT_ELEMENT, $ROOT_SCOPE, DOWNGRADED_MODULE_COUNT_KEY, UPGRADE_APP_TYPE_KEY} from './constants'; const DIRECTIVE_PREFIX_REGEXP = /^(?:x|data)[:\-_]/i; const DIRECTIVE_SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; @@ -42,6 +42,17 @@ export function controllerKey(name: string): string { return '$' + name + 'Controller'; } +// Destroy an AngularJS app given the app `$injector`. +// +// NOTE: Destroying an app is not officially supported by AngularJS, but we do our best. +export function destroyApp($injector: IInjectorService): void { + const $rootElement: IAugmentedJQuery = $injector.get($ROOT_ELEMENT); + const $rootScope: IRootScopeService = $injector.get($ROOT_SCOPE); + + $rootScope.$destroy(); + cleanData($rootElement[0] as Element); +} + export function directiveNormalize(name: string): string { return name.replace(DIRECTIVE_PREFIX_REGEXP, '') .replace(DIRECTIVE_SPECIAL_CHARS_REGEXP, (_, letter) => letter.toUpperCase()); diff --git a/packages/upgrade/src/dynamic/src/upgrade_adapter.ts b/packages/upgrade/src/dynamic/src/upgrade_adapter.ts index bbb304e494c00..beb6b5b3a462b 100644 --- a/packages/upgrade/src/dynamic/src/upgrade_adapter.ts +++ b/packages/upgrade/src/dynamic/src/upgrade_adapter.ts @@ -13,7 +13,7 @@ import {bootstrap, element as angularElement, IAngularBootstrapConfig, IAugmente import {$$TESTABILITY, $COMPILE, $INJECTOR, $ROOT_SCOPE, COMPILER_KEY, INJECTOR_KEY, LAZY_MODULE_REF, NG_ZONE_KEY, UPGRADE_APP_TYPE_KEY} from '../../common/src/constants'; import {downgradeComponent} from '../../common/src/downgrade_component'; import {downgradeInjectable} from '../../common/src/downgrade_injectable'; -import {controllerKey, Deferred, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util'; +import {controllerKey, Deferred, destroyApp, LazyModuleRef, onError, UpgradeAppType} from '../../common/src/util'; import {UpgradeNg1ComponentAdapterBuilder} from './upgrade_ng1_adapter'; @@ -620,6 +620,12 @@ export class UpgradeAdapter { rootScope.$on('$destroy', () => { subscription.unsubscribe(); }); + + // Destroy the AngularJS app once the associated `PlatformRef` is destroyed. + // This does not happen in a typical SPA scenario, but it might be useful for + // other usecases where desposing of an Angular/AngularJS app is necessary (such + // as micro-frontends, Hot Module Replacement (HMR), etc.). + platformRef.onDestroy(() => destroyApp(ng1Injector)); }); }) .catch((e) => this.ng2BootstrapDeferred.reject(e)); diff --git a/packages/upgrade/static/src/downgrade_module.ts b/packages/upgrade/static/src/downgrade_module.ts index bde2fe8db93c2..938a3ea06d91e 100644 --- a/packages/upgrade/static/src/downgrade_module.ts +++ b/packages/upgrade/static/src/downgrade_module.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, NgModuleFactory, NgModuleRef, StaticProvider} from '@angular/core'; +import {Injector, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider} from '@angular/core'; import {platformBrowser} from '@angular/platform-browser'; import {IInjectorService, IProvideService, module_ as angularModule} from '../../src/common/src/angular1'; import {$INJECTOR, $PROVIDE, DOWNGRADED_MODULE_COUNT_KEY, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants'; -import {getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util'; +import {destroyApp, getDowngradedModuleCount, isFunction, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util'; import {angular1Providers, setTempInjectorRef} from './angular1_providers'; import {NgAdapterInjector} from './util'; @@ -167,6 +167,12 @@ export function downgradeModule(moduleFactoryOrBootstrapFn: NgModuleFactory destroyApp($injector)); + return injector; }) }; diff --git a/packages/upgrade/static/src/upgrade_module.ts b/packages/upgrade/static/src/upgrade_module.ts index 899d5ab59c4c0..fb4c90763392b 100644 --- a/packages/upgrade/static/src/upgrade_module.ts +++ b/packages/upgrade/static/src/upgrade_module.ts @@ -6,11 +6,11 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, isDevMode, NgModule, NgZone, Testability} from '@angular/core'; +import {Injector, isDevMode, NgModule, NgZone, PlatformRef, Testability} from '@angular/core'; import {bootstrap, element as angularElement, IInjectorService, IIntervalService, IProvideService, ITestabilityService, module_ as angularModule} from '../../src/common/src/angular1'; import {$$TESTABILITY, $DELEGATE, $INJECTOR, $INTERVAL, $PROVIDE, INJECTOR_KEY, LAZY_MODULE_REF, UPGRADE_APP_TYPE_KEY, UPGRADE_MODULE_NAME} from '../../src/common/src/constants'; -import {controllerKey, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util'; +import {controllerKey, destroyApp, LazyModuleRef, UpgradeAppType} from '../../src/common/src/util'; import {angular1Providers, setTempInjectorRef} from './angular1_providers'; import {NgAdapterInjector} from './util'; @@ -155,7 +155,13 @@ export class UpgradeModule { /** The root `Injector` for the upgrade application. */ injector: Injector, /** The bootstrap zone for the upgrade application */ - public ngZone: NgZone) { + public ngZone: NgZone, + /** + * The owning `NgModuleRef`s `PlatformRef` instance. + * This is used to tie the lifecycle of the bootstrapped AngularJS apps to that of the Angular + * `PlatformRef`. + */ + private platformRef: PlatformRef) { this.injector = new NgAdapterInjector(injector); } @@ -272,6 +278,12 @@ export class UpgradeModule { $rootScope.$on('$destroy', () => { subscription.unsubscribe(); }); + + // Destroy the AngularJS app once the associated `PlatformRef` is destroyed. This + // does not happen in a typical SPA scenario, but it might be useful for other + // usecases where desposing of an Angular/AngularJS app is necessary (such as + // micro-frontends, Hot Module Replacement (HMR), etc.). + this.platformRef.onDestroy(() => destroyApp($injector)); }, 0); } ]);