Skip to content

Commit

Permalink
fix(upgrade): fix HMR by destroying the AngularJS app when `PlatformR…
Browse files Browse the repository at this point in the history
…ef` 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 angular#39935
  • Loading branch information
gkalpak committed Dec 8, 2020
1 parent e194eee commit 0417a1b
Show file tree
Hide file tree
Showing 5 changed files with 44 additions and 8 deletions.
1 change: 1 addition & 0 deletions packages/upgrade/src/common/src/constants.ts
Expand Up @@ -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';
Expand Down
15 changes: 13 additions & 2 deletions packages/upgrade/src/common/src/util.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
8 changes: 7 additions & 1 deletion packages/upgrade/src/dynamic/src/upgrade_adapter.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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));
Expand Down
10 changes: 8 additions & 2 deletions packages/upgrade/static/src/downgrade_module.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -167,6 +167,12 @@ export function downgradeModule<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T
injector = result.injector = new NgAdapterInjector(ref.injector);
injector.get($INJECTOR);

// 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.).
injector.get(PlatformRef).onDestroy(() => destroyApp($injector));

return injector;
})
};
Expand Down
18 changes: 15 additions & 3 deletions packages/upgrade/static/src/upgrade_module.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}
]);
Expand Down

0 comments on commit 0417a1b

Please sign in to comment.