-
Notifications
You must be signed in to change notification settings - Fork 26.5k
fix(upgrade): fix HMR for hybrid applications #40045
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
c1f5f66
8ff6319
e80c8f5
43e994d
885710a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Previously, trying to apply a change via Hot Module Replacement (HMR) in a hybrid app would result in an error. This was caused by not having the AngularJS app destroyed and thus trying to bootstrap an AngularJS app on the same element twice. This commit fixes HMR for hybrid apps by ensuring the AngularJS app is destroyed when the Angular `PlatformRef` is [destroyed][1] in the [`module.hot.dispose()` callback][2]. NOTE: For "ngUpgradeLite" apps (i.e. those using `downgradeModule()`), HMR will only work if the downgraded module has been bootstrapped and there is at least one Angular component present on the page. The is due to a combination of two facts: - The logic for setting up the listener that destroys the AngularJS app depends on the downgraded module's `NgModuleRef`, which is only available after the module has been bootstrapped. - The [HMR dispose logic][3] depends on having an Angular element (identified by the auto-geenrated `ng-version` attribute) present in the DOM in order to retrieve the Angular `PlatformRef`. [1]: https://github.com/angular/angular-cli/blob/205ea2b638f154291993bfd9e065cd66ff20503/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L75 [2]: https://github.com/angular/angular-cli/blob/205ea2b638f154291993bfd9e065cd66ff205033/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L31 [3]: https://github.com/angular/angular-cli/blob/205ea2b638f154291993bfd9e065cd66ff205033/packages/angular_devkit/build_angular/src/webpack/plugins/hmr/hmr-accept.ts#L116 Fixes #39935
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'; | ||
|
||
|
@@ -619,6 +619,12 @@ export class UpgradeAdapter { | |
rootScope.$on('$destroy', () => { | ||
subscription.unsubscribe(); | ||
}); | ||
|
||
// Destroy the AngularJS app once the Angular `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 Hot Module Replacement (HMR)). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be useful to link to #39935 in this comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
platformRef.onDestroy(() => destroyApp(ng1Injector)); | ||
}); | ||
}) | ||
.catch((e) => this.ng2BootstrapDeferred.reject(e)); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -86,7 +86,7 @@ withEachNg1Version(() => { | |
}); | ||
})); | ||
|
||
it('supports the compilerOptions argument', waitForAsync(() => { | ||
it('should support the compilerOptions argument', waitForAsync(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😍 |
||
const platformRef = platformBrowserDynamic(); | ||
spyOn(platformRef, 'bootstrapModule').and.callThrough(); | ||
spyOn(platformRef, 'bootstrapModuleFactory').and.callThrough(); | ||
|
@@ -120,6 +120,64 @@ withEachNg1Version(() => { | |
ref.dispose(); | ||
}); | ||
})); | ||
|
||
it('should destroy the AngularJS app when `PlatformRef` is destroyed', waitForAsync(() => { | ||
const platformRef = platformBrowserDynamic(); | ||
const adapter = new UpgradeAdapter(forwardRef(() => Ng2Module)); | ||
const ng1Module = angular.module_('ng1', []); | ||
|
||
@Component({selector: 'ng2', template: '<span>NG2</span>'}) | ||
class Ng2Component { | ||
} | ||
|
||
@NgModule({ | ||
declarations: [Ng2Component], | ||
imports: [BrowserModule], | ||
}) | ||
class Ng2Module { | ||
ngDoBootstrap() {} | ||
} | ||
|
||
ng1Module.component('ng1', {template: '<ng2></ng2>'}); | ||
ng1Module.directive('ng2', adapter.downgradeNg2Component(Ng2Component)); | ||
|
||
const element = html('<div><ng1></ng1></div>'); | ||
|
||
adapter.bootstrap(element, [ng1Module.name]).ready(ref => { | ||
const $rootScope: angular.IRootScopeService = ref.ng1Injector.get($ROOT_SCOPE); | ||
const rootScopeDestroySpy = spyOn($rootScope, '$destroy'); | ||
|
||
const appElem = angular.element(element); | ||
const ng1Elem = angular.element(element.querySelector('ng1') as Element); | ||
const ng2Elem = angular.element(element.querySelector('ng2') as Element); | ||
const ng2ChildElem = angular.element(element.querySelector('ng2 span') as Element); | ||
|
||
// Attach data to all elements. | ||
appElem.data!('testData', 1); | ||
ng1Elem.data!('testData', 2); | ||
ng2Elem.data!('testData', 3); | ||
ng2ChildElem.data!('testData', 4); | ||
|
||
// Verify data can be retrieved. | ||
expect(appElem.data!('testData')).toBe(1); | ||
expect(ng1Elem.data!('testData')).toBe(2); | ||
expect(ng2Elem.data!('testData')).toBe(3); | ||
expect(ng2ChildElem.data!('testData')).toBe(4); | ||
|
||
expect(rootScopeDestroySpy).not.toHaveBeenCalled(); | ||
|
||
// Destroy `PlatformRef`. | ||
platformRef.destroy(); | ||
|
||
// Verify `$rootScope` has been destroyed and data has been cleaned up. | ||
expect(rootScopeDestroySpy).toHaveBeenCalled(); | ||
|
||
expect(appElem.data!('testData')).toBeUndefined(); | ||
expect(ng1Elem.data!('testData')).toBeUndefined(); | ||
expect(ng2Elem.data!('testData')).toBeUndefined(); | ||
expect(ng2ChildElem.data!('testData')).toBeUndefined(); | ||
}); | ||
})); | ||
}); | ||
|
||
describe('bootstrap errors', () => { | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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<T>(moduleFactoryOrBootstrapFn: NgModuleFactory<T | |||||
injector = result.injector = new NgAdapterInjector(ref.injector); | ||||||
injector.get($INJECTOR); | ||||||
|
||||||
// Destroy the AngularJS app once the Angular `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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooops 😅 Fixed. |
||||||
// as Hot Module Replacement (HMR)). | ||||||
injector.get(PlatformRef).onDestroy(() => destroyApp($injector)); | ||||||
|
||||||
return injector; | ||||||
}) | ||||||
}; | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops 😅 Fixed.