Description
Which @angular/* package(s) are the source of the bug?
compiler
Is this a regression?
Yes
Description
After upgrading to ng15, I'm seeing bad compile output in async functions that are assigned to fields - the this
is not being captured, resulting in NPEs in a number of places. I've also seen other issues where field initialization is re-ordered after upgrading to ng15, which may be related.
A simple example is:
import { Inject, Injectable, InjectionToken, OnDestroy } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { delay } from './delay';
export const WINDOW = new InjectionToken<Window>('window', { providedIn: 'platform', factory: () => window });
/**
* Service that provides online/offline status.
*/
@Injectable({ providedIn: 'root' })
export class OnlineService2
extends BehaviorSubject<boolean>
implements OnDestroy {
constructor(@Inject(WINDOW) private readonly _window: Window) {
const browserOnline = _window.navigator.onLine;
super(browserOnline);
_window.addEventListener('online', this.toOnline);
_window.addEventListener('offline', this.toOffline);
}
ngOnDestroy() {
this._window.removeEventListener('online', this.toOnline);
this._window.removeEventListener('offline', this.toOffline);
this.complete();
}
private readonly toOnline = async () => {
await delay(1);
this.next(true);
};
private readonly toOffline = async () => {
await delay(1);
this.next(false);
};
}
The compiled output is:
class OnlineService2 extends rxjs__WEBPACK_IMPORTED_MODULE_1__.BehaviorSubject {
constructor() {
const browserOnline = navigator.onLine;
super(browserOnline);
window.addEventListener('online', this.toOnline);
window.addEventListener('offline', this.toOffline);
}
ngOnDestroy() {
window.removeEventListener('online', this.toOnline);
window.removeEventListener('offline', this.toOffline);
this.complete();
}
toOnline = (0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_2__.delay)(1);
_this.next(true);
});
toOffline = (0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_2__.delay)(1);
_this2.next(false);
});
static ɵfac = function OnlineService2_Factory(t) {
return new (t || OnlineService2)();
};
static ɵprov = /*@__PURE__*/_angular_core__WEBPACK_IMPORTED_MODULE_3__["ɵɵdefineInjectable"]({
token: OnlineService2,
factory: OnlineService2.ɵfac,
providedIn: 'root'
});
}
You can see that the _this
and _this2
are not initialized before they are used. If I change my tsconfig settings from:
"module": "esnext",
to
"module": "ES2022",
"useDefineForClassFields": false,
the compiled output works as expected:
class OnlineService2 extends rxjs__WEBPACK_IMPORTED_MODULE_2__.BehaviorSubject {
constructor(_window) {
var _this;
const browserOnline = _window.navigator.onLine;
super(browserOnline);
_this = this;
this._window = _window;
this.toOnline = /*#__PURE__*/(0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_3__.delay)(1);
_this.next(true);
});
this.toOffline = /*#__PURE__*/(0,prj_node_modules_angular_devkit_build_angular_node_modules_babel_runtime_helpers_esm_asyncToGenerator_js__WEBPACK_IMPORTED_MODULE_0__["default"])(function* () {
yield (0,_ease_util__WEBPACK_IMPORTED_MODULE_3__.delay)(1);
_this.next(false);
});
_window.addEventListener('online', this.toOnline);
_window.addEventListener('offline', this.toOffline);
}
ngOnDestroy() {
this._window.removeEventListener('online', this.toOnline);
this._window.removeEventListener('offline', this.toOffline);
this.complete();
}
static #_ = this.ɵfac = function OnlineService2_Factory(t) {
return new (t || OnlineService2)(_angular_core__WEBPACK_IMPORTED_MODULE_1__["ɵɵinject"](WINDOW));
};
static #_2 = this.ɵprov = /*@__PURE__*/_angular_core__WEBPACK_IMPORTED_MODULE_1__["ɵɵdefineInjectable"]({
token: OnlineService2,
factory: OnlineService2.ɵfac,
providedIn: 'root'
});
}
Please provide a link to a minimal reproduction of the bug
https://github.com/johncrim/repro-ng15-compile
Please provide the exception or error you saw
Error: Uncaught (in promise): ReferenceError: _this is not defined
ReferenceError: _this is not defined
at http://localhost:9876/_karma_webpack_/webpack:/apps/ease/src/modules/app/Online.service2.ts:37:5
at Generator.next (<anonymous>)
at asyncGeneratorStep (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:3:1)
at apply (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime/helpers/esm/asyncToGenerator.js:22:1)
at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:375:26)
at FakeAsyncTestZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:1938:33)
at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:284:39)
at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:374:52)
at Zone.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:134:43)
at apply (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:1278:36)
at _ZoneDelegate.invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:409:31)
at ProxyZoneSpec.onInvokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:318:39)
at _ZoneDelegate.invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:408:60)
at Zone.runTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:178:47)
at invokeTask (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:490:34)
Please provide the environment you discovered this bug in (run ng version
)
Angular CLI: 15.0.5
Node: 19.3.0 (Unsupported)
Package Manager: yarn 3.2.4
OS: win32 x64
Angular: 15.0.4
... animations, cdk, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Package Version
---------------------------------------------------------
@angular-devkit/architect 0.1500.5
@angular-devkit/build-angular 15.0.5
@angular-devkit/core 15.0.5
@angular-devkit/schematics 15.0.5
@angular/cli 15.0.5
@schematics/angular 15.0.5
ng-packagr 15.0.3
rxjs 7.8.0
typescript 4.8.4
webpack 5.75.0
Anything else?
I have not been able to cause the error in my standalone project (https://github.com/johncrim/repro-ng15-compile) - I've tried tweaking dependencies eg the typescript version and tsconfig setting to match my upgraded project, but no luck generating the failure. However if I move the same code into my project I am seeing the compile output described above.
I have a fix (in tsconfig set "module": "ES2022" and "useDefineForClassFields": false) which is easy to apply, though I'm not done testing it everywhere. Since those tsconfig settings weren't changed by migrations I'm raising the issue for the benefit of the community.