From fa320af64cd7fb594f9e9b452599f6a65476376b Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 5 Jun 2020 16:26:23 +0200 Subject: [PATCH] fix(compiler-cli): downlevel angular decorators to static properties (#37382) In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output and SSR due to a limitation in TypeScript. TypeScript by default preserves type information for decorated constructor parameters when `emitDecoratorMetadata` is enabled. For example, consider this snippet below: ``` @Directive() export class MyDirective { constructor(button: MyButton) {} } export class MyButton {} ``` TypeScript would generate metadata for the `MyDirective` class it has a decorator applied. This metadata would be needed in JIT mode, or for libraries that provide `MyDirective` through NPM. The metadata would look as followed: ``` let MyDirective = class MyDir {} MyDirective = __decorate([ Directive(), __metadata("design:paramtypes", [MyButton]), ], MyDirective); let MyButton = class MyButton {} ``` Notice that TypeScript generated calls to `__decorate` and `__metadata`. These calls are needed so that the Angular compiler is able to determine whether `MyDirective` is actually an directive, and what types are needed for dependency injection. The limitation surfaces in this concrete example because `MyButton` is declared after the `__metadata(..)` call, while `__metadata` actually directly references `MyButton`. This is illegal though because `MyButton` has not been declared at this point. This is due to the so-called temporal dead zone in JavaScript. Errors like followed will be reported at runtime when such file/code evaluates: ``` Uncaught ReferenceError: Cannot access 'MyButton' before initialization ``` As noted, this is a TypeScript limitation because ideally TypeScript shouldn't evaluate `__metadata`/reference `MyButton` immediately. Instead, it should defer the reference until `MyButton` is actually declared. This limitation will not be fixed by the TypeScript team though because it's a limitation as per current design and they will only revisit this once the tc39 decorator proposal is finalized (currently stage-2 at time of writing). Given this wontfix on the TypeScript side, and our heavy reliance on this metadata in libraries (and for JIT mode), we intend to fix this from within the Angular compiler by downleveling decorators to static properties that don't need to evaluate directly. For example: ``` MyDirective.ctorParameters = () => [MyButton]; ``` With this snippet above, `MyButton` is not referenced directly. Only lazily when the Angular runtime needs it. This mitigates the temporal dead zone issue caused by a limitation in TypeScript's decorator metadata output. See: https://github.com/microsoft/TypeScript/issues/27519. In the past (as noted; before version 7), the Angular compiler by default used tsickle that already performed this transformation. We moved the transformation to the CLI for JIT and `ng-packager`, but now we realize that we can move this all to a single place in the compiler so that standalone ngc consumers can benefit too, and that we can disable tsickle in our Bazel `ngc-wrapped` pipeline (that currently still relies on tsickle to perform this decorator processing). This transformation also has another positive side-effect of making Angular application/library code more compatible with server-side rendering. In principle, TypeScript would also preserve type information for decorated class members (similar to how it did that for constructor parameters) at runtime. This becomes an issue when your application relies on native DOM globals for decorated class member types. e.g. ``` @Input() panelElement: HTMLElement; ``` Your application code would then reference `HTMLElement` directly whenever the source file is loaded in NodeJS for SSR. `HTMLElement` does not exist on the server though, so that will become an invalid reference. One could work around this by providing global mocks for these DOM symbols, but that doesn't match up with other places where dependency injection is used for mocking DOM/browser specific symbols. More context in this issue: #30586. The TL;DR here is that the Angular compiler does not care about types for these class members, so it won't ever reference `HTMLElement` at runtime. Fixes #30106. Fixes #30586. Fixes #30141. Resolves FW-2196. Resolves FW-2199. PR Close #37382 --- packages/bazel/src/ngc-wrapped/index.ts | 48 +- .../test/ng_package/example_package.golden | 23 +- packages/compiler-cli/BUILD.bazel | 1 + packages/compiler-cli/src/main.ts | 65 +- .../src/ngtsc/core/src/compiler.ts | 2 +- packages/compiler-cli/src/tooling.ts | 19 + .../downlevel_decorators_transform.ts | 582 ++++++++++++++++ .../patch_alias_reference_resolution.ts | 120 ++++ .../compiler-cli/src/transformers/program.ts | 138 ++-- .../src/transformers/r3_strip_decorators.ts | 167 ----- packages/compiler-cli/test/ngc_spec.ts | 176 +++-- .../test/transformers/BUILD.bazel | 1 + .../downlevel_decorators_transform_spec.ts | 624 ++++++++++++++++++ .../bundling/todo_i18n/OUTSTANDING_WORK.md | 4 +- 14 files changed, 1550 insertions(+), 420 deletions(-) create mode 100644 packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts create mode 100644 packages/compiler-cli/src/transformers/patch_alias_reference_resolution.ts delete mode 100644 packages/compiler-cli/src/transformers/r3_strip_decorators.ts create mode 100644 packages/compiler-cli/test/transformers/downlevel_decorators_transform_spec.ts diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index eb9e9fa2f0ad4..c6127b4d914d7 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -191,23 +191,8 @@ export function compile({ fileLoader = new UncachedFileLoader(); } - compilerOpts.annotationsAs = 'static fields'; - if (!bazelOpts.es5Mode) { - if (bazelOpts.workspaceName === 'google3') { - compilerOpts.annotateForClosureCompiler = true; - } else { - compilerOpts.annotateForClosureCompiler = false; - } - } - // Detect from compilerOpts whether the entrypoint is being invoked in Ivy mode. const isInIvyMode = !!compilerOpts.enableIvy; - - // Disable downleveling and Closure annotation if in Ivy mode. - if (isInIvyMode) { - compilerOpts.annotationsAs = 'decorators'; - } - if (!compilerOpts.rootDirs) { throw new Error('rootDirs is not set!'); } @@ -264,9 +249,6 @@ export function compile({ } if (isInIvyMode) { - // Also need to disable decorator downleveling in the BazelHost in Ivy mode. - bazelHost.transformDecorators = false; - const delegate = bazelHost.shouldSkipTsickleProcessing.bind(bazelHost); bazelHost.shouldSkipTsickleProcessing = (fileName: string) => { // The base implementation of shouldSkipTsickleProcessing checks whether `fileName` is part of @@ -277,12 +259,36 @@ export function compile({ }; } + // By default, disable tsickle decorator transforming in the tsickle compiler host. + // The Angular compilers have their own logic for decorator processing and we wouldn't + // want tsickle to interfere with that. + bazelHost.transformDecorators = false; + + // By default in the `prodmode` output, we do not add annotations for closure compiler. + // Though, if we are building inside `google3`, closure annotations are desired for + // prodmode output, so we enable it by default. The defaults can be overridden by + // setting the `annotateForClosureCompiler` compiler option in the user tsconfig. + if (!bazelOpts.es5Mode) { + if (bazelOpts.workspaceName === 'google3') { + compilerOpts.annotateForClosureCompiler = true; + // Enable the tsickle decorator transform in google3 with Ivy mode enabled. The tsickle + // decorator transformation is still needed. This might be because of custom decorators + // with the `@Annotation` JSDoc that will be processed by the tsickle decorator transform. + // TODO: Figure out why this is needed in g3 and how we can improve this. FW-2225 + if (isInIvyMode) { + bazelHost.transformDecorators = true; + } + } else { + compilerOpts.annotateForClosureCompiler = false; + } + } + + // The `annotateForClosureCompiler` Angular compiler option is not respected by default + // as ngc-wrapped handles tsickle emit on its own. This means that we need to update + // the tsickle compiler host based on the `annotateForClosureCompiler` flag. if (compilerOpts.annotateForClosureCompiler) { bazelHost.transformTypesToClosure = true; } - if (compilerOpts.annotateForClosureCompiler || compilerOpts.annotationsAs === 'static fields') { - bazelHost.transformDecorators = true; - } const origBazelHostFileExist = bazelHost.fileExists; bazelHost.fileExists = (fileName: string) => { diff --git a/packages/bazel/test/ng_package/example_package.golden b/packages/bazel/test/ng_package/example_package.golden index ed9853d15619b..5f5668bb3d124 100644 --- a/packages/bazel/test/ng_package/example_package.golden +++ b/packages/bazel/test/ng_package/example_package.golden @@ -253,10 +253,10 @@ Hello var MySecondService = /** @class */ (function () { function MySecondService() { } + MySecondService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); MySecondService.decorators = [ { type: i0.Injectable, args: [{ providedIn: 'root' },] } ]; - MySecondService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); return MySecondService; }()); @@ -271,14 +271,13 @@ Hello function MyService(secondService) { this.secondService = secondService; } + MyService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(i0.ɵɵinject(MySecondService)); }, token: MyService, providedIn: "root" }); MyService.decorators = [ { type: i0.Injectable, args: [{ providedIn: 'root' },] } ]; - /** @nocollapse */ MyService.ctorParameters = function () { return [ { type: MySecondService } ]; }; - MyService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(i0.ɵɵinject(MySecondService)); }, token: MyService, providedIn: "root" }); return MyService; }()); @@ -317,7 +316,7 @@ Hello * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license - */var n=function(){function e(){}return e.decorators=[{type:t.Injectable,args:[{providedIn:"root"}]}],e.ɵprov=t.ɵɵdefineInjectable({factory:function t(){return new e},token:e,providedIn:"root"}),e}(),r=function(){function e(e){this.secondService=e}return e.decorators=[{type:t.Injectable,args:[{providedIn:"root"}]}],e.ctorParameters=function(){return[{type:n}]},e.ɵprov=t.ɵɵdefineInjectable({factory:function r(){return new e(t.ɵɵinject(n))},token:e,providedIn:"root"}),e}(); + */var n=function(){function e(){}return e.ɵprov=t.ɵɵdefineInjectable({factory:function t(){return new e},token:e,providedIn:"root"}),e.decorators=[{type:t.Injectable,args:[{providedIn:"root"}]}],e}(),r=function(){function e(e){this.secondService=e}return e.ɵprov=t.ɵɵdefineInjectable({factory:function r(){return new e(t.ɵɵinject(n))},token:e,providedIn:"root"}),e.decorators=[{type:t.Injectable,args:[{providedIn:"root"}]}],e.ctorParameters=function(){return[{type:n}]},e}(); /** * @license * Copyright Google LLC All Rights Reserved. @@ -602,18 +601,17 @@ let MyService = /** @class */ (() => { this.secondService = secondService; } } + MyService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(i0.ɵɵinject(i1.MySecondService)); }, token: MyService, providedIn: "root" }); MyService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; - /** @nocollapse */ MyService.ctorParameters = () => [ { type: MySecondService } ]; - MyService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(i0.ɵɵinject(i1.MySecondService)); }, token: MyService, providedIn: "root" }); return MyService; })(); export { MyService }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL2JhemVsL3Rlc3QvbmdfcGFja2FnZS9leGFtcGxlL2ltcG9ydHMvcHVibGljLWFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxVQUFVLENBQUM7OztBQUV6QztJQUFBLE1BQ2EsU0FBUztRQUNwQixZQUFtQixhQUE4QjtZQUE5QixrQkFBYSxHQUFiLGFBQWEsQ0FBaUI7UUFBRyxDQUFDOzs7Z0JBRnRELFVBQVUsU0FBQyxFQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUM7Ozs7Z0JBRnhCLGVBQWU7OztvQkFUdkI7S0FjQztTQUZZLFNBQVMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIEBsaWNlbnNlXG4gKiBDb3B5cmlnaHQgR29vZ2xlIExMQyBBbGwgUmlnaHRzIFJlc2VydmVkLlxuICpcbiAqIFVzZSBvZiB0aGlzIHNvdXJjZSBjb2RlIGlzIGdvdmVybmVkIGJ5IGFuIE1JVC1zdHlsZSBsaWNlbnNlIHRoYXQgY2FuIGJlXG4gKiBmb3VuZCBpbiB0aGUgTElDRU5TRSBmaWxlIGF0IGh0dHBzOi8vYW5ndWxhci5pby9saWNlbnNlXG4gKi9cblxuaW1wb3J0IHtJbmplY3RhYmxlfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB7TXlTZWNvbmRTZXJ2aWNlfSBmcm9tICcuL3NlY29uZCc7XG5cbkBJbmplY3RhYmxlKHtwcm92aWRlZEluOiAncm9vdCd9KVxuZXhwb3J0IGNsYXNzIE15U2VydmljZSB7XG4gIGNvbnN0cnVjdG9yKHB1YmxpYyBzZWNvbmRTZXJ2aWNlOiBNeVNlY29uZFNlcnZpY2UpIHt9XG59XG4iXX0= +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL2JhemVsL3Rlc3QvbmdfcGFja2FnZS9leGFtcGxlL2ltcG9ydHMvcHVibGljLWFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxVQUFVLENBQUM7OztBQUV6QztJQUFBLE1BQ2EsU0FBUztRQUNwQixZQUFtQixhQUE4QjtZQUE5QixrQkFBYSxHQUFiLGFBQWEsQ0FBaUI7UUFBRyxDQUFDOzs7O2dCQUZ0RCxVQUFVLFNBQUMsRUFBQyxVQUFVLEVBQUUsTUFBTSxFQUFDOzs7Z0JBRnhCLGVBQWU7O29CQVR2QjtLQWNDO1NBRlksU0FBUyIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGxpY2Vuc2VcbiAqIENvcHlyaWdodCBHb29nbGUgTExDIEFsbCBSaWdodHMgUmVzZXJ2ZWQuXG4gKlxuICogVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgYW4gTUlULXN0eWxlIGxpY2Vuc2UgdGhhdCBjYW4gYmVcbiAqIGZvdW5kIGluIHRoZSBMSUNFTlNFIGZpbGUgYXQgaHR0cHM6Ly9hbmd1bGFyLmlvL2xpY2Vuc2VcbiAqL1xuXG5pbXBvcnQge0luamVjdGFibGV9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuaW1wb3J0IHtNeVNlY29uZFNlcnZpY2V9IGZyb20gJy4vc2Vjb25kJztcblxuQEluamVjdGFibGUoe3Byb3ZpZGVkSW46ICdyb290J30pXG5leHBvcnQgY2xhc3MgTXlTZXJ2aWNlIHtcbiAgY29uc3RydWN0b3IocHVibGljIHNlY29uZFNlcnZpY2U6IE15U2Vjb25kU2VydmljZSkge31cbn1cbiJdfQ== --- esm2015/imports/second.js --- @@ -629,14 +627,14 @@ import * as i0 from "@angular/core"; let MySecondService = /** @class */ (() => { class MySecondService { } + MySecondService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); MySecondService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; - MySecondService.ɵprov = i0.ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); return MySecondService; })(); export { MySecondService }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vjb25kLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vcGFja2FnZXMvYmF6ZWwvdGVzdC9uZ19wYWNrYWdlL2V4YW1wbGUvaW1wb3J0cy9zZWNvbmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7OztHQU1HO0FBRUgsT0FBTyxFQUFDLFVBQVUsRUFBQyxNQUFNLGVBQWUsQ0FBQzs7QUFFekM7SUFBQSxNQUNhLGVBQWU7OztnQkFEM0IsVUFBVSxTQUFDLEVBQUMsVUFBVSxFQUFFLE1BQU0sRUFBQzs7OzBCQVZoQztLQVlDO1NBRFksZUFBZSIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGxpY2Vuc2VcbiAqIENvcHlyaWdodCBHb29nbGUgTExDIEFsbCBSaWdodHMgUmVzZXJ2ZWQuXG4gKlxuICogVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgYW4gTUlULXN0eWxlIGxpY2Vuc2UgdGhhdCBjYW4gYmVcbiAqIGZvdW5kIGluIHRoZSBMSUNFTlNFIGZpbGUgYXQgaHR0cHM6Ly9hbmd1bGFyLmlvL2xpY2Vuc2VcbiAqL1xuXG5pbXBvcnQge0luamVjdGFibGV9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuXG5ASW5qZWN0YWJsZSh7cHJvdmlkZWRJbjogJ3Jvb3QnfSlcbmV4cG9ydCBjbGFzcyBNeVNlY29uZFNlcnZpY2Uge1xufVxuIl19 +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2Vjb25kLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vLi4vcGFja2FnZXMvYmF6ZWwvdGVzdC9uZ19wYWNrYWdlL2V4YW1wbGUvaW1wb3J0cy9zZWNvbmQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7OztHQU1HO0FBRUgsT0FBTyxFQUFDLFVBQVUsRUFBQyxNQUFNLGVBQWUsQ0FBQzs7QUFFekM7SUFBQSxNQUNhLGVBQWU7Ozs7Z0JBRDNCLFVBQVUsU0FBQyxFQUFDLFVBQVUsRUFBRSxNQUFNLEVBQUM7OzBCQVZoQztLQVlDO1NBRFksZUFBZSIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGxpY2Vuc2VcbiAqIENvcHlyaWdodCBHb29nbGUgTExDIEFsbCBSaWdodHMgUmVzZXJ2ZWQuXG4gKlxuICogVXNlIG9mIHRoaXMgc291cmNlIGNvZGUgaXMgZ292ZXJuZWQgYnkgYW4gTUlULXN0eWxlIGxpY2Vuc2UgdGhhdCBjYW4gYmVcbiAqIGZvdW5kIGluIHRoZSBMSUNFTlNFIGZpbGUgYXQgaHR0cHM6Ly9hbmd1bGFyLmlvL2xpY2Vuc2VcbiAqL1xuXG5pbXBvcnQge0luamVjdGFibGV9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xuXG5ASW5qZWN0YWJsZSh7cHJvdmlkZWRJbjogJ3Jvb3QnfSlcbmV4cG9ydCBjbGFzcyBNeVNlY29uZFNlcnZpY2Uge1xufVxuIl19 --- esm2015/index.js --- @@ -800,7 +798,7 @@ export { A11yModule }; * License: MIT */ -import { Injectable, ɵɵdefineInjectable, ɵɵinject } from '@angular/core'; +import { ɵɵdefineInjectable, Injectable, ɵɵinject } from '@angular/core'; /** * @license @@ -812,10 +810,10 @@ import { Injectable, ɵɵdefineInjectable, ɵɵinject } from '@angular/core'; let MySecondService = /** @class */ (() => { class MySecondService { } + MySecondService.ɵprov = ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); MySecondService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; - MySecondService.ɵprov = ɵɵdefineInjectable({ factory: function MySecondService_Factory() { return new MySecondService(); }, token: MySecondService, providedIn: "root" }); return MySecondService; })(); @@ -832,14 +830,13 @@ let MyService = /** @class */ (() => { this.secondService = secondService; } } + MyService.ɵprov = ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(ɵɵinject(MySecondService)); }, token: MyService, providedIn: "root" }); MyService.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; - /** @nocollapse */ MyService.ctorParameters = () => [ { type: MySecondService } ]; - MyService.ɵprov = ɵɵdefineInjectable({ factory: function MyService_Factory() { return new MyService(ɵɵinject(MySecondService)); }, token: MyService, providedIn: "root" }); return MyService; })(); diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index fa0e14e6e0411..2ad66dfa6aff3 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -29,6 +29,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/perf", + "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/typecheck", "@npm//@bazel/typescript", "@npm//@types/node", diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index a629759a4b824..0c8eb8ddb899f 100644 --- a/packages/compiler-cli/src/main.ts +++ b/packages/compiler-cli/src/main.ts @@ -89,18 +89,9 @@ export function mainDiagnosticsForTest( } function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|undefined { - const transformDecorators = - (options.enableIvy === false && options.annotationsAs !== 'decorators'); - const transformTypesToClosure = options.annotateForClosureCompiler; - if (!transformDecorators && !transformTypesToClosure) { + if (!options.annotateForClosureCompiler) { return undefined; } - if (transformDecorators) { - // This is needed as a workaround for https://github.com/angular/tsickle/issues/635 - // Otherwise tsickle might emit references to non imported values - // as TypeScript elided the import. - options.emitDecoratorMetadata = true; - } const tsickleHost: Pick< tsickle.TsickleHost, 'shouldSkipTsickleProcessing'|'pathToModuleName'|'shouldIgnoreWarningsForPath'| @@ -115,41 +106,29 @@ function createEmitCallback(options: api.CompilerOptions): api.TsEmitCallback|un googmodule: false, untyped: true, convertIndexImportShorthand: false, - transformDecorators, - transformTypesToClosure, + // Decorators are transformed as part of the Angular compiler programs. To avoid + // conflicts, we disable decorator transformations for tsickle. + transformDecorators: false, + transformTypesToClosure: true, }; - if (options.annotateForClosureCompiler || options.annotationsAs === 'static fields') { - return ({ - program, - targetSourceFile, - writeFile, - cancellationToken, - emitOnlyDtsFiles, - customTransformers = {}, - host, - options - }) => - // tslint:disable-next-line:no-require-imports only depend on tsickle if requested - require('tsickle').emitWithTsickle( - program, {...tsickleHost, options, host, moduleResolutionHost: host}, host, options, - targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, { - beforeTs: customTransformers.before, - afterTs: customTransformers.after, - }); - } else { - return ({ - program, - targetSourceFile, - writeFile, - cancellationToken, - emitOnlyDtsFiles, - customTransformers = {}, - }) => - program.emit( - targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, - {after: customTransformers.after, before: customTransformers.before}); - } + return ({ + program, + targetSourceFile, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + customTransformers = {}, + host, + options + }) => + // tslint:disable-next-line:no-require-imports only depend on tsickle if requested + require('tsickle').emitWithTsickle( + program, {...tsickleHost, options, host, moduleResolutionHost: host}, host, options, + targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, { + beforeTs: customTransformers.before, + afterTs: customTransformers.after, + }); } export interface NgcParsedConfiguration extends ParsedConfiguration { diff --git a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts index 0a54b3d9d5909..a52b17c34d0a8 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -752,7 +752,7 @@ export class NgCompiler { /** * Determine if the given `Program` is @angular/core. */ -function isAngularCorePackage(program: ts.Program): boolean { +export function isAngularCorePackage(program: ts.Program): boolean { // Look for its_just_angular.ts somewhere in the program. const r3Symbols = getR3SymbolsFile(program); if (r3Symbols === null) { diff --git a/packages/compiler-cli/src/tooling.ts b/packages/compiler-cli/src/tooling.ts index 44a7d50e21050..b986f0df956c6 100644 --- a/packages/compiler-cli/src/tooling.ts +++ b/packages/compiler-cli/src/tooling.ts @@ -13,7 +13,10 @@ * Any changes to this file should be discussed with the Angular CLI team. */ +import * as ts from 'typescript'; +import {TypeScriptReflectionHost} from './ngtsc/reflection'; +import {getDownlevelDecoratorsTransform} from './transformers/downlevel_decorators_transform'; /** * Known values for global variables in `@angular/core` that Terser should set using @@ -28,3 +31,19 @@ export const GLOBAL_DEFS_FOR_TERSER_WITH_AOT = { ...GLOBAL_DEFS_FOR_TERSER, ngJitMode: false, }; + +/** + * Transform for downleveling Angular decorators and Angular-decorated class constructor + * parameters for dependency injection. This transform can be used by the CLI for JIT-mode + * compilation where decorators should be preserved, but downleveled so that apps are not + * exposed to the ES2015 temporal dead zone limitation in TypeScript's metadata. + * See https://github.com/angular/angular-cli/pull/14473 for more details. + */ +export function decoratorDownlevelTransformerFactory(program: ts.Program): + ts.TransformerFactory { + const typeChecker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + return getDownlevelDecoratorsTransform( + typeChecker, reflectionHost, [], /* isCore */ false, + /* enableClosureCompiler */ false); +} diff --git a/packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts b/packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts new file mode 100644 index 0000000000000..ad19c49d44956 --- /dev/null +++ b/packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts @@ -0,0 +1,582 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; +import {Decorator, ReflectionHost} from '../ngtsc/reflection'; +import {isAliasImportDeclaration, patchAliasReferenceResolutionOrDie} from './patch_alias_reference_resolution'; + +/** + * Whether a given decorator should be treated as an Angular decorator. + * Either it's used in @angular/core, or it's imported from there. + */ +function isAngularDecorator(decorator: Decorator, isCore: boolean): boolean { + return isCore || (decorator.import !== null && decorator.import.from === '@angular/core'); +} + +/* + ##################################################################### + Code below has been extracted from the tsickle decorator downlevel transformer + and a few local modifications have been applied: + + 1. Tsickle by default processed all decorators that had the `@Annotation` JSDoc. + We modified the transform to only be concerned with known Angular decorators. + 2. Tsickle by default added `@nocollapse` to all generated `ctorParameters` properties. + We only do this when `annotateForClosureCompiler` is enabled. + 3. Tsickle does not handle union types for dependency injection. i.e. if a injected type + is denoted with `@Optional`, the actual type could be set to `T | null`. + See: https://github.com/angular/angular-cli/commit/826803d0736b807867caff9f8903e508970ad5e4. + 4. Tsickle relied on `emitDecoratorMetadata` to be set to `true`. This is due to a limitation + in TypeScript transformers that never has been fixed. We were able to work around this + limitation so that `emitDecoratorMetadata` doesn't need to be specified. + See: `patchAliasReferenceResolution` for more details. + + Here is a link to the tsickle revision on which this transformer is based: + https://github.com/angular/tsickle/blob/fae06becb1570f491806060d83f29f2d50c43cdd/src/decorator_downlevel_transformer.ts + ##################################################################### +*/ + +/** + * Creates the AST for the decorator field type annotation, which has the form + * { type: Function, args?: any[] }[] + */ +function createDecoratorInvocationType(): ts.TypeNode { + const typeElements: ts.TypeElement[] = []; + typeElements.push(ts.createPropertySignature( + undefined, 'type', undefined, + ts.createTypeReferenceNode(ts.createIdentifier('Function'), undefined), undefined)); + typeElements.push(ts.createPropertySignature( + undefined, 'args', ts.createToken(ts.SyntaxKind.QuestionToken), + ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)), undefined)); + return ts.createArrayTypeNode(ts.createTypeLiteralNode(typeElements)); +} + +/** + * Extracts the type of the decorator (the function or expression invoked), as well as all the + * arguments passed to the decorator. Returns an AST with the form: + * + * // For @decorator(arg1, arg2) + * { type: decorator, args: [arg1, arg2] } + */ +function extractMetadataFromSingleDecorator( + decorator: ts.Decorator, diagnostics: ts.Diagnostic[]): ts.ObjectLiteralExpression { + const metadataProperties: ts.ObjectLiteralElementLike[] = []; + const expr = decorator.expression; + switch (expr.kind) { + case ts.SyntaxKind.Identifier: + // The decorator was a plain @Foo. + metadataProperties.push(ts.createPropertyAssignment('type', expr)); + break; + case ts.SyntaxKind.CallExpression: + // The decorator was a call, like @Foo(bar). + const call = expr as ts.CallExpression; + metadataProperties.push(ts.createPropertyAssignment('type', call.expression)); + if (call.arguments.length) { + const args: ts.Expression[] = []; + for (const arg of call.arguments) { + args.push(arg); + } + const argsArrayLiteral = ts.createArrayLiteral(args); + argsArrayLiteral.elements.hasTrailingComma = true; + metadataProperties.push(ts.createPropertyAssignment('args', argsArrayLiteral)); + } + break; + default: + diagnostics.push({ + file: decorator.getSourceFile(), + start: decorator.getStart(), + length: decorator.getEnd() - decorator.getStart(), + messageText: + `${ts.SyntaxKind[decorator.kind]} not implemented in gathering decorator metadata.`, + category: ts.DiagnosticCategory.Error, + code: 0, + }); + break; + } + return ts.createObjectLiteral(metadataProperties); +} + +/** + * Takes a list of decorator metadata object ASTs and produces an AST for a + * static class property of an array of those metadata objects. + */ +function createDecoratorClassProperty(decoratorList: ts.ObjectLiteralExpression[]) { + const modifier = ts.createToken(ts.SyntaxKind.StaticKeyword); + const type = createDecoratorInvocationType(); + const initializer = ts.createArrayLiteral(decoratorList, true); + // NB: the .decorators property does not get a @nocollapse property. There is + // no good reason why - it means .decorators is not runtime accessible if you + // compile with collapse properties, whereas propDecorators is, which doesn't + // follow any stringent logic. However this has been the case previously, and + // adding it back in leads to substantial code size increases as Closure fails + // to tree shake these props without @nocollapse. + return ts.createProperty(undefined, [modifier], 'decorators', undefined, type, initializer); +} + +/** + * Creates the AST for the 'ctorParameters' field type annotation: + * () => ({ type: any, decorators?: {type: Function, args?: any[]}[] }|null)[] + */ +function createCtorParametersClassPropertyType(): ts.TypeNode { + // Sorry about this. Try reading just the string literals below. + const typeElements: ts.TypeElement[] = []; + typeElements.push(ts.createPropertySignature( + undefined, 'type', undefined, + ts.createTypeReferenceNode(ts.createIdentifier('any'), undefined), undefined)); + typeElements.push(ts.createPropertySignature( + undefined, 'decorators', ts.createToken(ts.SyntaxKind.QuestionToken), + ts.createArrayTypeNode(ts.createTypeLiteralNode([ + ts.createPropertySignature( + undefined, 'type', undefined, + ts.createTypeReferenceNode(ts.createIdentifier('Function'), undefined), undefined), + ts.createPropertySignature( + undefined, 'args', ts.createToken(ts.SyntaxKind.QuestionToken), + ts.createArrayTypeNode( + ts.createTypeReferenceNode(ts.createIdentifier('any'), undefined)), + undefined), + ])), + undefined)); + return ts.createFunctionTypeNode( + undefined, [], + ts.createArrayTypeNode( + ts.createUnionTypeNode([ts.createTypeLiteralNode(typeElements), ts.createNull()]))); +} + +/** + * Sets a Closure \@nocollapse synthetic comment on the given node. This prevents Closure Compiler + * from collapsing the apparently static property, which would make it impossible to find for code + * trying to detect it at runtime. + */ +function addNoCollapseComment(n: ts.Node) { + ts.setSyntheticLeadingComments(n, [{ + kind: ts.SyntaxKind.MultiLineCommentTrivia, + text: '* @nocollapse ', + pos: -1, + end: -1, + hasTrailingNewLine: true + }]); +} + +/** + * createCtorParametersClassProperty creates a static 'ctorParameters' property containing + * downleveled decorator information. + * + * The property contains an arrow function that returns an array of object literals of the shape: + * static ctorParameters = () => [{ + * type: SomeClass|undefined, // the type of the param that's decorated, if it's a value. + * decorators: [{ + * type: DecoratorFn, // the type of the decorator that's invoked. + * args: [ARGS], // the arguments passed to the decorator. + * }] + * }]; + */ +function createCtorParametersClassProperty( + diagnostics: ts.Diagnostic[], + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, + ctorParameters: ParameterDecorationInfo[], + isClosureCompilerEnabled: boolean): ts.PropertyDeclaration { + const params: ts.Expression[] = []; + + for (const ctorParam of ctorParameters) { + if (!ctorParam.type && ctorParam.decorators.length === 0) { + params.push(ts.createNull()); + continue; + } + + const paramType = ctorParam.type ? + typeReferenceToExpression(entityNameToExpression, ctorParam.type) : + undefined; + const members = + [ts.createPropertyAssignment('type', paramType || ts.createIdentifier('undefined'))]; + + const decorators: ts.ObjectLiteralExpression[] = []; + for (const deco of ctorParam.decorators) { + decorators.push(extractMetadataFromSingleDecorator(deco, diagnostics)); + } + if (decorators.length) { + members.push(ts.createPropertyAssignment('decorators', ts.createArrayLiteral(decorators))); + } + params.push(ts.createObjectLiteral(members)); + } + + const initializer = ts.createArrowFunction( + undefined, undefined, [], undefined, ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.createArrayLiteral(params, true)); + const type = createCtorParametersClassPropertyType(); + const ctorProp = ts.createProperty( + undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], 'ctorParameters', undefined, type, + initializer); + if (isClosureCompilerEnabled) { + addNoCollapseComment(ctorProp); + } + return ctorProp; +} + +/** + * createPropDecoratorsClassProperty creates a static 'propDecorators' property containing type + * information for every property that has a decorator applied. + * + * static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { + * propA: [{type: MyDecorator, args: [1, 2]}, ...], + * ... + * }; + */ +function createPropDecoratorsClassProperty( + diagnostics: ts.Diagnostic[], properties: Map): ts.PropertyDeclaration { + // `static propDecorators: {[key: string]: ` + {type: Function, args?: any[]}[] + `} = {\n`); + const entries: ts.ObjectLiteralElementLike[] = []; + for (const [name, decorators] of properties.entries()) { + entries.push(ts.createPropertyAssignment( + name, + ts.createArrayLiteral( + decorators.map(deco => extractMetadataFromSingleDecorator(deco, diagnostics))))); + } + const initializer = ts.createObjectLiteral(entries, true); + const type = ts.createTypeLiteralNode([ts.createIndexSignature( + undefined, undefined, [ts.createParameter( + undefined, undefined, undefined, 'key', undefined, + ts.createTypeReferenceNode('string', undefined), undefined)], + createDecoratorInvocationType())]); + return ts.createProperty( + undefined, [ts.createToken(ts.SyntaxKind.StaticKeyword)], 'propDecorators', undefined, type, + initializer); +} + +/** + * Returns an expression representing the (potentially) value part for the given node. + * + * This is a partial re-implementation of TypeScript's serializeTypeReferenceNode. This is a + * workaround for https://github.com/Microsoft/TypeScript/issues/17516 (serializeTypeReferenceNode + * not being exposed). In practice this implementation is sufficient for Angular's use of type + * metadata. + */ +function typeReferenceToExpression( + entityNameToExpression: (n: ts.EntityName) => ts.Expression | undefined, + node: ts.TypeNode): ts.Expression|undefined { + let kind = node.kind; + if (ts.isLiteralTypeNode(node)) { + // Treat literal types like their base type (boolean, string, number). + kind = node.literal.kind; + } + switch (kind) { + case ts.SyntaxKind.FunctionType: + case ts.SyntaxKind.ConstructorType: + return ts.createIdentifier('Function'); + case ts.SyntaxKind.ArrayType: + case ts.SyntaxKind.TupleType: + return ts.createIdentifier('Array'); + case ts.SyntaxKind.TypePredicate: + case ts.SyntaxKind.TrueKeyword: + case ts.SyntaxKind.FalseKeyword: + case ts.SyntaxKind.BooleanKeyword: + return ts.createIdentifier('Boolean'); + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.StringKeyword: + return ts.createIdentifier('String'); + case ts.SyntaxKind.ObjectKeyword: + return ts.createIdentifier('Object'); + case ts.SyntaxKind.NumberKeyword: + case ts.SyntaxKind.NumericLiteral: + return ts.createIdentifier('Number'); + case ts.SyntaxKind.TypeReference: + const typeRef = node as ts.TypeReferenceNode; + // Ignore any generic types, just return the base type. + return entityNameToExpression(typeRef.typeName); + case ts.SyntaxKind.UnionType: + const childTypeNodes = + (node as ts.UnionTypeNode).types.filter(t => t.kind !== ts.SyntaxKind.NullKeyword); + return childTypeNodes.length === 1 ? + typeReferenceToExpression(entityNameToExpression, childTypeNodes[0]) : + undefined; + default: + return undefined; + } +} + +/** + * Returns true if the given symbol refers to a value (as distinct from a type). + * + * Expands aliases, which is important for the case where + * import * as x from 'some-module'; + * and x is now a value (the module object). + */ +function symbolIsValue(tc: ts.TypeChecker, sym: ts.Symbol): boolean { + if (sym.flags & ts.SymbolFlags.Alias) sym = tc.getAliasedSymbol(sym); + return (sym.flags & ts.SymbolFlags.Value) !== 0; +} + +/** ParameterDecorationInfo describes the information for a single constructor parameter. */ +interface ParameterDecorationInfo { + /** + * The type declaration for the parameter. Only set if the type is a value (e.g. a class, not an + * interface). + */ + type: ts.TypeNode|null; + /** The list of decorators found on the parameter, null if none. */ + decorators: ts.Decorator[]; +} + +/** + * Gets a transformer for downleveling Angular decorators. + * @param typeChecker Reference to the program's type checker. + * @param host Reflection host that is used for determining decorators. + * @param diagnostics List which will be populated with diagnostics if any. + * @param isCore Whether the current TypeScript program is for the `@angular/core` package. + * @param isClosureCompilerEnabled Whether closure annotations need to be added where needed. + */ +export function getDownlevelDecoratorsTransform( + typeChecker: ts.TypeChecker, host: ReflectionHost, diagnostics: ts.Diagnostic[], + isCore: boolean, isClosureCompilerEnabled: boolean): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + let referencedParameterTypes = new Set(); + + /** + * Converts an EntityName (from a type annotation) to an expression (accessing a value). + * + * For a given qualified name, this walks depth first to find the leftmost identifier, + * and then converts the path into a property access that can be used as expression. + */ + function entityNameToExpression(name: ts.EntityName): ts.Expression|undefined { + const symbol = typeChecker.getSymbolAtLocation(name); + // Check if the entity name references a symbol that is an actual value. If it is not, it + // cannot be referenced by an expression, so return undefined. + if (!symbol || !symbolIsValue(typeChecker, symbol) || !symbol.declarations || + symbol.declarations.length === 0) { + return undefined; + } + // If we deal with a qualified name, build up a property access expression + // that could be used in the JavaScript output. + if (ts.isQualifiedName(name)) { + const containerExpr = entityNameToExpression(name.left); + if (containerExpr === undefined) { + return undefined; + } + return ts.createPropertyAccess(containerExpr, name.right); + } + const decl = symbol.declarations[0]; + // If the given entity name has been resolved to an alias import declaration, + // ensure that the alias declaration is not elided by TypeScript, and use its + // name identifier to reference it at runtime. + if (isAliasImportDeclaration(decl)) { + referencedParameterTypes.add(decl); + // If the entity name resolves to an alias import declaration, we reference the + // entity based on the alias import name. This ensures that TypeScript properly + // resolves the link to the import. Cloning the original entity name identifier + // could lead to an incorrect resolution at local scope. e.g. Consider the following + // snippet: `constructor(Dep: Dep) {}`. In such a case, the local `Dep` identifier + // would resolve to the actual parameter name, and not to the desired import. + // This happens because the entity name identifier symbol is internally considered + // as type-only and therefore TypeScript tries to resolve it as value manually. + // We can help TypeScript and avoid this non-reliable resolution by using an identifier + // that is not type-only and is directly linked to the import alias declaration. + if (decl.name !== undefined) { + return ts.getMutableClone(decl.name); + } + } + // Clone the original entity name identifier so that it can be used to reference + // its value at runtime. This is used when the identifier is resolving to a file + // local declaration (otherwise it would resolve to an alias import declaration). + return ts.getMutableClone(name); + } + + /** + * Transforms a class element. Returns a three tuple of name, transformed element, and + * decorators found. Returns an undefined name if there are no decorators to lower on the + * element, or the element has an exotic name. + */ + function transformClassElement(element: ts.ClassElement): + [string|undefined, ts.ClassElement, ts.Decorator[]] { + element = ts.visitEachChild(element, decoratorDownlevelVisitor, context); + const decoratorsToKeep: ts.Decorator[] = []; + const toLower: ts.Decorator[] = []; + const decorators = host.getDecoratorsOfDeclaration(element) || []; + for (const decorator of decorators) { + // We only deal with concrete nodes in TypeScript sources, so we don't + // need to handle synthetically created decorators. + const decoratorNode = decorator.node! as ts.Decorator; + if (!isAngularDecorator(decorator, isCore)) { + decoratorsToKeep.push(decoratorNode); + continue; + } + toLower.push(decoratorNode); + } + if (!toLower.length) return [undefined, element, []]; + + if (!element.name || !ts.isIdentifier(element.name)) { + // Method has a weird name, e.g. + // [Symbol.foo]() {...} + diagnostics.push({ + file: element.getSourceFile(), + start: element.getStart(), + length: element.getEnd() - element.getStart(), + messageText: `Cannot process decorators for class element with non-analyzable name.`, + category: ts.DiagnosticCategory.Error, + code: 0, + }); + return [undefined, element, []]; + } + + const name = (element.name as ts.Identifier).text; + const mutable = ts.getMutableClone(element); + mutable.decorators = decoratorsToKeep.length ? + ts.setTextRange(ts.createNodeArray(decoratorsToKeep), mutable.decorators) : + undefined; + return [name, mutable, toLower]; + } + + /** + * Transforms a constructor. Returns the transformed constructor and the list of parameter + * information collected, consisting of decorators and optional type. + */ + function transformConstructor(ctor: ts.ConstructorDeclaration): + [ts.ConstructorDeclaration, ParameterDecorationInfo[]] { + ctor = ts.visitEachChild(ctor, decoratorDownlevelVisitor, context); + + const newParameters: ts.ParameterDeclaration[] = []; + const oldParameters = + ts.visitParameterList(ctor.parameters, decoratorDownlevelVisitor, context); + const parametersInfo: ParameterDecorationInfo[] = []; + for (const param of oldParameters) { + const decoratorsToKeep: ts.Decorator[] = []; + const paramInfo: ParameterDecorationInfo = {decorators: [], type: null}; + const decorators = host.getDecoratorsOfDeclaration(param) || []; + + for (const decorator of decorators) { + // We only deal with concrete nodes in TypeScript sources, so we don't + // need to handle synthetically created decorators. + const decoratorNode = decorator.node! as ts.Decorator; + if (!isAngularDecorator(decorator, isCore)) { + decoratorsToKeep.push(decoratorNode); + continue; + } + paramInfo!.decorators.push(decoratorNode); + } + if (param.type) { + // param has a type provided, e.g. "foo: Bar". + // The type will be emitted as a value expression in entityNameToExpression, which takes + // care not to emit anything for types that cannot be expressed as a value (e.g. + // interfaces). + paramInfo!.type = param.type; + } + parametersInfo.push(paramInfo); + const newParam = ts.updateParameter( + param, + // Must pass 'undefined' to avoid emitting decorator metadata. + decoratorsToKeep.length ? decoratorsToKeep : undefined, param.modifiers, + param.dotDotDotToken, param.name, param.questionToken, param.type, param.initializer); + newParameters.push(newParam); + } + const updated = ts.updateConstructor( + ctor, ctor.decorators, ctor.modifiers, newParameters, + ts.visitFunctionBody(ctor.body, decoratorDownlevelVisitor, context)); + return [updated, parametersInfo]; + } + + /** + * Transforms a single class declaration: + * - dispatches to strip decorators on members + * - converts decorators on the class to annotations + * - creates a ctorParameters property + * - creates a propDecorators property + */ + function transformClassDeclaration(classDecl: ts.ClassDeclaration): ts.ClassDeclaration { + classDecl = ts.getMutableClone(classDecl); + + const newMembers: ts.ClassElement[] = []; + const decoratedProperties = new Map(); + let classParameters: ParameterDecorationInfo[]|null = null; + + for (const member of classDecl.members) { + switch (member.kind) { + case ts.SyntaxKind.PropertyDeclaration: + case ts.SyntaxKind.GetAccessor: + case ts.SyntaxKind.SetAccessor: + case ts.SyntaxKind.MethodDeclaration: { + const [name, newMember, decorators] = transformClassElement(member); + newMembers.push(newMember); + if (name) decoratedProperties.set(name, decorators); + continue; + } + case ts.SyntaxKind.Constructor: { + const ctor = member as ts.ConstructorDeclaration; + if (!ctor.body) break; + const [newMember, parametersInfo] = + transformConstructor(member as ts.ConstructorDeclaration); + classParameters = parametersInfo; + newMembers.push(newMember); + continue; + } + default: + break; + } + newMembers.push(ts.visitEachChild(member, decoratorDownlevelVisitor, context)); + } + const decorators = host.getDecoratorsOfDeclaration(classDecl) || []; + + const decoratorsToLower = []; + const decoratorsToKeep: ts.Decorator[] = []; + for (const decorator of decorators) { + // We only deal with concrete nodes in TypeScript sources, so we don't + // need to handle synthetically created decorators. + const decoratorNode = decorator.node! as ts.Decorator; + if (isAngularDecorator(decorator, isCore)) { + decoratorsToLower.push(extractMetadataFromSingleDecorator(decoratorNode, diagnostics)); + } else { + decoratorsToKeep.push(decoratorNode); + } + } + + const newClassDeclaration = ts.getMutableClone(classDecl); + + if (decoratorsToLower.length) { + newMembers.push(createDecoratorClassProperty(decoratorsToLower)); + } + if (classParameters) { + if ((decoratorsToLower.length) || classParameters.some(p => !!p.decorators.length)) { + // emit ctorParameters if the class was decoratored at all, or if any of its ctors + // were classParameters + newMembers.push(createCtorParametersClassProperty( + diagnostics, entityNameToExpression, classParameters, isClosureCompilerEnabled)); + } + } + if (decoratedProperties.size) { + newMembers.push(createPropDecoratorsClassProperty(diagnostics, decoratedProperties)); + } + newClassDeclaration.members = ts.setTextRange( + ts.createNodeArray(newMembers, newClassDeclaration.members.hasTrailingComma), + classDecl.members); + newClassDeclaration.decorators = + decoratorsToKeep.length ? ts.createNodeArray(decoratorsToKeep) : undefined; + return newClassDeclaration; + } + + /** + * Transformer visitor that looks for Angular decorators and replaces them with + * downleveled static properties. Also collects constructor type metadata for + * class declaration that are decorated with an Angular decorator. + */ + function decoratorDownlevelVisitor(node: ts.Node): ts.Node { + if (ts.isClassDeclaration(node)) { + return transformClassDeclaration(node); + } + return ts.visitEachChild(node, decoratorDownlevelVisitor, context); + } + + return (sf: ts.SourceFile) => { + // Ensure that referenced type symbols are not elided by TypeScript. Imports for + // such parameter type symbols previously could be type-only, but now might be also + // used in the `ctorParameters` static property as a value. We want to make sure + // that TypeScript does not elide imports for such type references. Read more + // about this in the description for `patchAliasReferenceResolution`. + patchAliasReferenceResolutionOrDie(context, referencedParameterTypes); + // Downlevel decorators and constructor parameter types. We will keep track of all + // referenced constructor parameter types so that we can instruct TypeScript to + // not elide their imports if they previously were only type-only. + return ts.visitEachChild(sf, decoratorDownlevelVisitor, context); + }; + }; +} diff --git a/packages/compiler-cli/src/transformers/patch_alias_reference_resolution.ts b/packages/compiler-cli/src/transformers/patch_alias_reference_resolution.ts new file mode 100644 index 0000000000000..f1ccea42d86ad --- /dev/null +++ b/packages/compiler-cli/src/transformers/patch_alias_reference_resolution.ts @@ -0,0 +1,120 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +/** + * Describes a TypeScript transformation context with the internal emit + * resolver exposed. There are requests upstream in TypeScript to expose + * that as public API: https://github.com/microsoft/TypeScript/issues/17516.. + */ +interface TransformationContextWithResolver extends ts.TransformationContext { + getEmitResolver: () => EmitResolver; +} + +/** Describes a subset of the TypeScript internal emit resolver. */ +interface EmitResolver { + isReferencedAliasDeclaration?(node: ts.Node, checkChildren?: boolean): void; +} + +/** + * Patches the alias declaration reference resolution for a given transformation context + * so that TypeScript knows about the specified alias declarations being referenced. + * + * This exists because TypeScript performs analysis of import usage before transformers + * run and doesn't refresh its state after transformations. This means that imports + * for symbols used as constructor types are elided due to their original type-only usage. + * + * In reality though, since we downlevel decorators and constructor parameters, we want + * these symbols to be retained in the JavaScript output as they will be used as values + * at runtime. We can instruct TypeScript to preserve imports for such identifiers by + * creating a mutable clone of a given import specifier/clause or namespace, but that + * has the downside of preserving the full import in the JS output. See: + * https://github.com/microsoft/TypeScript/blob/3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/src/compiler/transformers/ts.ts#L242-L250. + * + * This is a trick the CLI used in the past for constructor parameter downleveling in JIT: + * https://github.com/angular/angular-cli/blob/b3f84cc5184337666ce61c07b7b9df418030106f/packages/ngtools/webpack/src/transformers/ctor-parameters.ts#L323-L325 + * The trick is not ideal though as it preserves the full import (as outlined before), and it + * results in a slow-down due to the type checker being involved multiple times. The CLI + * worked around this import preserving issue by having another complex post-process step that + * detects and elides unused imports. Note that these unused imports could cause unused chunks + * being generated by Webpack if the application or library is not marked as side-effect free. + * + * This is not ideal though, as we basically re-implement the complex import usage resolution + * from TypeScript. We can do better by letting TypeScript do the import eliding, but providing + * information about the alias declarations (e.g. import specifiers) that should not be elided + * because they are actually referenced (as they will now appear in static properties). + * + * More information about these limitations with transformers can be found in: + * 1. https://github.com/Microsoft/TypeScript/issues/17552. + * 2. https://github.com/microsoft/TypeScript/issues/17516. + * 3. https://github.com/angular/tsickle/issues/635. + * + * The patch we apply to tell TypeScript about actual referenced aliases (i.e. imported symbols), + * matches conceptually with the logic that runs internally in TypeScript when the + * `emitDecoratorMetadata` flag is enabled. TypeScript basically surfaces the same problem and + * solves it conceptually the same way, but obviously doesn't need to access an `@internal` API. + * + * See below. Note that this uses sourcegraph as the TypeScript checker file doesn't display on + * Github. + * https://sourcegraph.com/github.com/microsoft/TypeScript@3eaa7c65f6f076a08a5f7f1946fd0df7c7430259/-/blob/src/compiler/checker.ts#L31219-31257 + */ +export function patchAliasReferenceResolutionOrDie( + context: ts.TransformationContext, referencedAliases: Set): void { + // If the `getEmitResolver` method is not available, TS most likely changed the + // internal structure of the transformation context. We will abort gracefully. + if (!isTransformationContextWithEmitResolver(context)) { + throwIncompatibleTransformationContextError(); + return; + } + const emitResolver = context.getEmitResolver(); + const originalReferenceResolution = emitResolver.isReferencedAliasDeclaration; + // If the emit resolver does not have a function called `isReferencedAliasDeclaration`, then + // we abort gracefully as most likely TS changed the internal structure of the emit resolver. + if (originalReferenceResolution === undefined) { + throwIncompatibleTransformationContextError(); + return; + } + emitResolver.isReferencedAliasDeclaration = function(node, ...args) { + if (isAliasImportDeclaration(node) && referencedAliases.has(node)) { + return true; + } + return originalReferenceResolution.call(emitResolver, node, ...args); + }; +} + +/** + * Gets whether a given node corresponds to an import alias declaration. Alias + * declarations can be import specifiers, namespace imports or import clauses + * as these do not declare an actual symbol but just point to a target declaration. + */ +export function isAliasImportDeclaration(node: ts.Node): node is ts.ImportSpecifier| + ts.NamespaceImport|ts.ImportClause { + return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node); +} + +/** Whether the transformation context exposes its emit resolver. */ +function isTransformationContextWithEmitResolver(context: ts.TransformationContext): + context is TransformationContextWithResolver { + return (context as Partial).getEmitResolver !== undefined; +} + + +/** + * Throws an error about an incompatible TypeScript version for which the alias + * declaration reference resolution could not be monkey-patched. The error will + * also propose potential solutions that can be applied by developers. + */ +function throwIncompatibleTransformationContextError() { + throw Error( + 'Unable to downlevel Angular decorators due to an incompatible TypeScript ' + + 'version.\nIf you recently updated TypeScript and this issue surfaces now, consider ' + + 'downgrading.\n\n' + + 'Please report an issue on the Angular repositories when this issue ' + + 'surfaces and you are using a supposedly compatible TypeScript version.'); +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index dbc3dcdb709f7..fa3467e5e902f 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -7,26 +7,28 @@ * found in the LICENSE file at https://angular.io/license */ -import {AotCompiler, AotCompilerHost, AotCompilerOptions, core, createAotCompiler, EmitterVisitorContext, FormattedMessageChain, GeneratedFile, getParseErrors, isFormattedError, isSyntaxError, MessageBundle, NgAnalyzedFile, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Position, Serializer, StaticSymbol, TypeScriptEmitter, Xliff, Xliff2, Xmb} from '@angular/compiler'; +import {AotCompiler, AotCompilerOptions, core, createAotCompiler, FormattedMessageChain, GeneratedFile, getParseErrors, isFormattedError, isSyntaxError, MessageBundle, NgAnalyzedFileWithInjectables, NgAnalyzedModules, ParseSourceSpan, PartialModule, Serializer, Xliff, Xliff2, Xmb} from '@angular/compiler'; import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {translateDiagnostics, TypeCheckHost} from '../diagnostics/translate_diagnostics'; -import {createBundleIndexHost, MetadataCollector, ModuleMetadata} from '../metadata'; +import {translateDiagnostics} from '../diagnostics/translate_diagnostics'; +import {createBundleIndexHost, MetadataCollector} from '../metadata'; +import {isAngularCorePackage} from '../ngtsc/core/src/compiler'; import {NgtscProgram} from '../ngtsc/program'; +import {TypeScriptReflectionHost} from '../ngtsc/reflection'; import {verifySupportedTypeScriptVersion} from '../typescript_support'; -import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback, TsMergeEmitResultsCallback} from './api'; +import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, DiagnosticMessageChain, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitCallback, TsMergeEmitResultsCallback} from './api'; import {CodeGenerator, getOriginalReferences, TsCompilerAotCompilerTypeCheckHostAdapter} from './compiler_host'; +import {getDownlevelDecoratorsTransform} from './downlevel_decorators_transform'; import {getInlineResourcesTransformFactory, InlineResourcesMetadataTransformer} from './inline_resources'; import {getExpressionLoweringTransformFactory, LowerMetadataTransform} from './lower_expressions'; import {MetadataCache, MetadataTransformer} from './metadata_cache'; import {getAngularEmitterTransformFactory} from './node_emitter_transform'; import {PartialModuleMetadataTransformer} from './r3_metadata_transform'; -import {getDecoratorStripTransformerFactory, StripDecoratorsMetadataTransformer} from './r3_strip_decorators'; import {getAngularClassTransformerFactory} from './r3_transform'; -import {createMessageDiagnostic, DTS, GENERATED_FILES, isInRootDir, ngToTsDiagnostic, StructureIsReused, TS, tsStructureIsReused, userError} from './util'; +import {createMessageDiagnostic, DTS, GENERATED_FILES, isInRootDir, ngToTsDiagnostic, StructureIsReused, TS, tsStructureIsReused} from './util'; /** @@ -46,14 +48,6 @@ const LOWER_FIELDS = ['useValue', 'useFactory', 'data', 'id', 'loadChildren']; */ const R3_LOWER_FIELDS = [...LOWER_FIELDS, 'providers', 'imports', 'exports']; -const R3_REIFIED_DECORATORS = [ - 'Component', - 'Directive', - 'Injectable', - 'NgModule', - 'Pipe', -]; - const emptyModules: NgAnalyzedModules = { ngModules: [], ngModuleByPipeOrDirective: new Map(), @@ -99,8 +93,7 @@ class AngularCompilerProgram implements Program { private _structuralDiagnostics: Diagnostic[]|undefined; private _programWithStubs: ts.Program|undefined; private _optionsDiagnostics: Diagnostic[] = []; - // TODO(issue/24571): remove '!'. - private _reifiedDecorators!: Set; + private _transformTsDiagnostics: ts.Diagnostic[] = []; constructor( rootNames: ReadonlyArray, private options: CompilerOptions, @@ -263,72 +256,6 @@ class AngularCompilerProgram implements Program { return this._emitRender2(parameters); } - private _emitRender3({ - emitFlags = EmitFlags.Default, - cancellationToken, - customTransformers, - emitCallback = defaultEmitCallback, - mergeEmitResultsCallback = mergeEmitResults, - }: { - emitFlags?: EmitFlags, - cancellationToken?: ts.CancellationToken, - customTransformers?: CustomTransformers, - emitCallback?: TsEmitCallback, - mergeEmitResultsCallback?: TsMergeEmitResultsCallback, - } = {}): ts.EmitResult { - const emitStart = Date.now(); - if ((emitFlags & (EmitFlags.JS | EmitFlags.DTS | EmitFlags.Metadata | EmitFlags.Codegen)) === - 0) { - return {emitSkipped: true, diagnostics: [], emittedFiles: []}; - } - - // analyzedModules and analyzedInjectables are created together. If one exists, so does the - // other. - const modules = - this.compiler.emitAllPartialModules(this.analyzedModules, this._analyzedInjectables!); - - const writeTsFile: ts.WriteFileCallback = - (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { - this.writeFile(outFileName, outData, writeByteOrderMark, onError, undefined, sourceFiles); - }; - - const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS; - - const tsCustomTransformers = this.calculateTransforms( - /* genFiles */ undefined, /* partialModules */ modules, - /* stripDecorators */ this.reifiedDecorators, customTransformers); - - - // Restore the original references before we emit so TypeScript doesn't emit - // a reference to the .d.ts file. - const augmentedReferences = new Map>(); - for (const sourceFile of this.tsProgram.getSourceFiles()) { - const originalReferences = getOriginalReferences(sourceFile); - if (originalReferences) { - augmentedReferences.set(sourceFile, sourceFile.referencedFiles); - sourceFile.referencedFiles = originalReferences; - } - } - - try { - return emitCallback({ - program: this.tsProgram, - host: this.host, - options: this.options, - writeFile: writeTsFile, - emitOnlyDtsFiles, - customTransformers: tsCustomTransformers - }); - } finally { - // Restore the references back to the augmented value to ensure that the - // checks that TypeScript makes for project structure reuse will succeed. - for (const [sourceFile, references] of Array.from(augmentedReferences)) { - // TODO(chuckj): Remove any cast after updating build to 2.6 - (sourceFile as any).referencedFiles = references; - } - } - } - private _emitRender2({ emitFlags = EmitFlags.Default, cancellationToken, @@ -367,6 +294,7 @@ class AngularCompilerProgram implements Program { const genFileByFileName = new Map(); genFiles.forEach(genFile => genFileByFileName.set(genFile.genFileUrl, genFile)); this.emittedLibrarySummaries = []; + this._transformTsDiagnostics = []; const emittedSourceFiles = [] as ts.SourceFile[]; const writeTsFile: ts.WriteFileCallback = (outFileName, outData, writeByteOrderMark, onError?, sourceFiles?) => { @@ -389,8 +317,8 @@ class AngularCompilerProgram implements Program { const modules = this._analyzedInjectables && this.compiler.emitAllPartialModules2(this._analyzedInjectables); - const tsCustomTransformers = this.calculateTransforms( - genFileByFileName, modules, /* stripDecorators */ undefined, customTransformers); + const tsCustomTransformers = + this.calculateTransforms(genFileByFileName, modules, customTransformers); const emitOnlyDtsFiles = (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS; // Restore the original references before we emit so TypeScript doesn't emit // a reference to the .d.ts file. @@ -548,22 +476,23 @@ class AngularCompilerProgram implements Program { return this._tsProgram!; } - private get reifiedDecorators(): Set { - if (!this._reifiedDecorators) { - const reflector = this.compiler.reflector; - this._reifiedDecorators = new Set( - R3_REIFIED_DECORATORS.map(name => reflector.findDeclaration('@angular/core', name))); + /** Whether the program is compiling the Angular core package. */ + private get isCompilingAngularCore(): boolean { + if (this._isCompilingAngularCore !== null) { + return this._isCompilingAngularCore; } - return this._reifiedDecorators; + return this._isCompilingAngularCore = isAngularCorePackage(this.tsProgram); } + private _isCompilingAngularCore: boolean|null = null; private calculateTransforms( genFiles: Map|undefined, partialModules: PartialModule[]|undefined, - stripDecorators: Set|undefined, customTransformers?: CustomTransformers): ts.CustomTransformers { const beforeTs: Array> = []; const metadataTransforms: MetadataTransformer[] = []; const flatModuleMetadataTransforms: MetadataTransformer[] = []; + const annotateForClosureCompiler = this.options.annotateForClosureCompiler || false; + if (this.options.enableResourceInlining) { beforeTs.push(getInlineResourcesTransformFactory(this.tsProgram, this.hostAdapter)); const transformer = new InlineResourcesMetadataTransformer(this.hostAdapter); @@ -576,7 +505,6 @@ class AngularCompilerProgram implements Program { getExpressionLoweringTransformFactory(this.loweringMetadataTransform, this.tsProgram)); metadataTransforms.push(this.loweringMetadataTransform); } - const annotateForClosureCompiler = this.options.annotateForClosureCompiler || false; if (genFiles) { beforeTs.push(getAngularEmitterTransformFactory( genFiles, this.getTsProgram(), annotateForClosureCompiler)); @@ -591,18 +519,26 @@ class AngularCompilerProgram implements Program { flatModuleMetadataTransforms.push(transformer); } - if (stripDecorators) { - beforeTs.push(getDecoratorStripTransformerFactory( - stripDecorators, this.compiler.reflector, this.getTsProgram().getTypeChecker())); - const transformer = - new StripDecoratorsMetadataTransformer(stripDecorators, this.compiler.reflector); - metadataTransforms.push(transformer); - flatModuleMetadataTransforms.push(transformer); - } - if (customTransformers && customTransformers.beforeTs) { beforeTs.push(...customTransformers.beforeTs); } + + // If decorators should be converted to static fields (enabled by default), we set up + // the decorator downlevel transform. Note that we set it up as last transform as that + // allows custom transformers to strip Angular decorators without having to deal with + // identifying static properties. e.g. it's more difficult handling `<..>.decorators` + // or `<..>.ctorParameters` compared to the `ts.Decorator` AST nodes. + if (this.options.annotationsAs !== 'decorators') { + const typeChecker = this.getTsProgram().getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + // Similarly to how we handled tsickle decorator downleveling in the past, we just + // ignore diagnostics that have been collected by the transformer. These are + // non-significant failures that shouldn't prevent apps from compiling. + beforeTs.push(getDownlevelDecoratorsTransform( + typeChecker, reflectionHost, [], this.isCompilingAngularCore, + annotateForClosureCompiler)); + } + if (metadataTransforms.length > 0) { this.metadataCache = this.createMetadataCache(metadataTransforms); } diff --git a/packages/compiler-cli/src/transformers/r3_strip_decorators.ts b/packages/compiler-cli/src/transformers/r3_strip_decorators.ts deleted file mode 100644 index 03f480e707540..0000000000000 --- a/packages/compiler-cli/src/transformers/r3_strip_decorators.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {StaticReflector, StaticSymbol} from '@angular/compiler'; -import * as ts from 'typescript'; - -import {isClassMetadata, isMetadataImportedSymbolReferenceExpression, isMetadataSymbolicCallExpression, MetadataValue} from '../metadata'; - -import {MetadataTransformer, ValueTransform} from './metadata_cache'; - -export type Transformer = (sourceFile: ts.SourceFile) => ts.SourceFile; -export type TransformerFactory = (context: ts.TransformationContext) => Transformer; - -export function getDecoratorStripTransformerFactory( - coreDecorators: Set, reflector: StaticReflector, - checker: ts.TypeChecker): TransformerFactory { - return function(context: ts.TransformationContext) { - return function(sourceFile: ts.SourceFile): ts.SourceFile { - const stripDecoratorsFromClassDeclaration = - (node: ts.ClassDeclaration): ts.ClassDeclaration => { - if (node.decorators === undefined) { - return node; - } - const decorators = node.decorators.filter(decorator => { - const callExpr = decorator.expression; - if (ts.isCallExpression(callExpr)) { - const id = callExpr.expression; - if (ts.isIdentifier(id)) { - const symbol = resolveToStaticSymbol(id, sourceFile.fileName, reflector, checker); - return symbol && coreDecorators.has(symbol); - } - } - return true; - }); - if (decorators.length !== node.decorators.length) { - return ts.updateClassDeclaration( - node, - decorators, - node.modifiers, - node.name, - node.typeParameters, - node.heritageClauses || [], - node.members, - ); - } - return node; - }; - - const stripDecoratorPropertyAssignment = (node: ts.ClassDeclaration): ts.ClassDeclaration => { - return ts.visitEachChild(node, member => { - if (!ts.isPropertyDeclaration(member) || !isDecoratorAssignment(member) || - !member.initializer || !ts.isArrayLiteralExpression(member.initializer)) { - return member; - } - - const newInitializer = ts.visitEachChild(member.initializer, decorator => { - if (!ts.isObjectLiteralExpression(decorator)) { - return decorator; - } - const type = lookupProperty(decorator, 'type'); - if (!type || !ts.isIdentifier(type)) { - return decorator; - } - const symbol = resolveToStaticSymbol(type, sourceFile.fileName, reflector, checker); - if (!symbol || !coreDecorators.has(symbol)) { - return decorator; - } - return undefined; - }, context); - - if (newInitializer === member.initializer) { - return member; - } else if (newInitializer.elements.length === 0) { - return undefined; - } else { - return ts.updateProperty( - member, member.decorators, member.modifiers, member.name, member.questionToken, - member.type, newInitializer); - } - }, context); - }; - - return ts.visitEachChild(sourceFile, stmt => { - if (ts.isClassDeclaration(stmt)) { - let decl = stmt; - if (stmt.decorators) { - decl = stripDecoratorsFromClassDeclaration(stmt); - } - return stripDecoratorPropertyAssignment(decl); - } - return stmt; - }, context); - }; - }; -} - -function isDecoratorAssignment(member: ts.ClassElement): boolean { - if (!ts.isPropertyDeclaration(member)) { - return false; - } - if (!member.modifiers || - !member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) { - return false; - } - if (!ts.isIdentifier(member.name) || member.name.text !== 'decorators') { - return false; - } - if (!member.initializer || !ts.isArrayLiteralExpression(member.initializer)) { - return false; - } - return true; -} - -function lookupProperty(expr: ts.ObjectLiteralExpression, prop: string): ts.Expression|undefined { - const decl = expr.properties.find( - elem => !!elem.name && ts.isIdentifier(elem.name) && elem.name.text === prop); - if (decl === undefined || !ts.isPropertyAssignment(decl)) { - return undefined; - } - return decl.initializer; -} - -function resolveToStaticSymbol( - id: ts.Identifier, containingFile: string, reflector: StaticReflector, - checker: ts.TypeChecker): StaticSymbol|null { - const res = checker.getSymbolAtLocation(id); - if (!res || !res.declarations || res.declarations.length === 0) { - return null; - } - const decl = res.declarations[0]; - if (!ts.isImportSpecifier(decl)) { - return null; - } - const moduleSpecifier = decl.parent!.parent!.parent!.moduleSpecifier; - if (!ts.isStringLiteral(moduleSpecifier)) { - return null; - } - return reflector.tryFindDeclaration(moduleSpecifier.text, id.text, containingFile); -} - -export class StripDecoratorsMetadataTransformer implements MetadataTransformer { - constructor(private coreDecorators: Set, private reflector: StaticReflector) {} - - start(sourceFile: ts.SourceFile): ValueTransform|undefined { - return (value: MetadataValue, node: ts.Node): MetadataValue => { - if (isClassMetadata(value) && ts.isClassDeclaration(node) && value.decorators) { - value.decorators = value.decorators.filter(d => { - if (isMetadataSymbolicCallExpression(d) && - isMetadataImportedSymbolReferenceExpression(d.expression)) { - const declaration = this.reflector.tryFindDeclaration( - d.expression.module, d.expression.name, sourceFile.fileName); - if (declaration && this.coreDecorators.has(declaration)) { - return false; - } - } - return true; - }); - } - return value; - }; - } -} diff --git a/packages/compiler-cli/test/ngc_spec.ts b/packages/compiler-cli/test/ngc_spec.ts index 8eee29092b6d3..b310c5adf705a 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -10,7 +10,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; -import {main, readCommandLineAndConfiguration, watchMode} from '../src/main'; +import {main, mainDiagnosticsForTest, readCommandLineAndConfiguration, watchMode} from '../src/main'; import {setup, stripAnsi} from './test_support'; describe('ngc transformer command-line', () => { @@ -97,6 +97,103 @@ describe('ngc transformer command-line', () => { expect(exitCode).toBe(1); }); + describe('decorator metadata', () => { + it('should add metadata as decorators if "annotationsAs" is set to "decorators"', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "emitDecoratorMetadata": true + }, + "angularCompilerOptions": { + "annotationsAs": "decorators" + }, + "files": ["mymodule.ts"] + }`); + write('aclass.ts', `export class AClass {}`); + write('mymodule.ts', ` + import {NgModule} from '@angular/core'; + import {AClass} from './aclass'; + + @NgModule({declarations: []}) + export class MyModule { + constructor(importedClass: AClass) {} + } + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('MyModule = __decorate(['); + expect(mymoduleSource).toContain(`import { AClass } from './aclass';`); + expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`); + expect(mymoduleSource).not.toContain('MyModule.ctorParameters'); + expect(mymoduleSource).not.toContain('MyModule.decorators'); + }); + + it('should add metadata for Angular-decorated classes as static fields', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["mymodule.ts"] + }`); + write('aclass.ts', `export class AClass {}`); + write('mymodule.ts', ` + import {NgModule} from '@angular/core'; + import {AClass} from './aclass'; + + @NgModule({declarations: []}) + export class MyModule { + constructor(importedClass: AClass) {} + } + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).not.toContain('__decorate'); + expect(mymoduleSource).toContain('args: [{ declarations: [] },] }'); + expect(mymoduleSource).not.toContain(`__metadata`); + expect(mymoduleSource).toContain(`import { AClass } from './aclass';`); + expect(mymoduleSource).toContain(`{ type: AClass }`); + }); + + it('should not downlevel decorators for classes with custom decorators', () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "files": ["mymodule.ts"] + }`); + write('aclass.ts', `export class AClass {}`); + write('decorator.ts', ` + export function CustomDecorator(metadata: any) { + return (...args: any[]) => {} + } + `); + write('mymodule.ts', ` + import {AClass} from './aclass'; + import {CustomDecorator} from './decorator'; + + @CustomDecorator({declarations: []}) + export class MyModule { + constructor(importedClass: AClass) {} + } + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(exitCode).toEqual(0); + + const mymodulejs = path.resolve(outDir, 'mymodule.js'); + const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); + expect(mymoduleSource).toContain('__decorate'); + expect(mymoduleSource).toContain('({ declarations: [] })'); + expect(mymoduleSource).not.toContain('AClass'); + expect(mymoduleSource).not.toContain('.ctorParameters ='); + expect(mymoduleSource).not.toContain('.decorators = '); + }); + }); + describe('errors', () => { beforeEach(() => { errorSpy.and.stub(); @@ -557,8 +654,6 @@ describe('ngc transformer command-line', () => { const mymodulejs = path.resolve(outDir, 'mymodule.js'); const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); expect(mymoduleSource).not.toContain('@fileoverview added by tsickle'); - expect(mymoduleSource).toContain('MyComp = __decorate'); - expect(mymoduleSource).not.toContain('MyComp.decorators = ['); }); it('should add closure annotations', () => { @@ -570,10 +665,14 @@ describe('ngc transformer command-line', () => { "files": ["mymodule.ts"] }`); write('mymodule.ts', ` - import {NgModule, Component} from '@angular/core'; + import {NgModule, Component, Injectable} from '@angular/core'; + + @Injectable() + export class InjectedClass {} @Component({template: ''}) export class MyComp { + constructor(injected: InjectedClass) {} fn(p: any) {} } @@ -588,74 +687,7 @@ describe('ngc transformer command-line', () => { const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); expect(mymoduleSource).toContain('@fileoverview added by tsickle'); expect(mymoduleSource).toContain('@param {?} p'); - }); - - it('should add metadata as decorators', () => { - writeConfig(`{ - "extends": "./tsconfig-base.json", - "compilerOptions": { - "emitDecoratorMetadata": true - }, - "angularCompilerOptions": { - "annotationsAs": "decorators" - }, - "files": ["mymodule.ts"] - }`); - write('aclass.ts', `export class AClass {}`); - write('mymodule.ts', ` - import {NgModule} from '@angular/core'; - import {AClass} from './aclass'; - - @NgModule({declarations: []}) - export class MyModule { - constructor(importedClass: AClass) {} - } - `); - - const exitCode = main(['-p', basePath], errorSpy); - expect(exitCode).toEqual(0); - - const mymodulejs = path.resolve(outDir, 'mymodule.js'); - const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); - expect(mymoduleSource).toContain('MyModule = __decorate(['); - expect(mymoduleSource).toContain(`import { AClass } from './aclass';`); - expect(mymoduleSource).toContain(`__metadata("design:paramtypes", [AClass])`); - }); - - it('should add metadata as static fields', () => { - // Note: Don't specify emitDecoratorMetadata here on purpose, - // as regression test for https://github.com/angular/angular/issues/19916. - writeConfig(`{ - "extends": "./tsconfig-base.json", - "compilerOptions": { - "emitDecoratorMetadata": false - }, - "angularCompilerOptions": { - "annotationsAs": "static fields" - }, - "files": ["mymodule.ts"] - }`); - write('aclass.ts', `export class AClass {}`); - write('mymodule.ts', ` - import {NgModule} from '@angular/core'; - import {AClass} from './aclass'; - - @NgModule({declarations: []}) - export class MyModule { - constructor(importedClass: AClass) {} - } - `); - - const exitCode = main(['-p', basePath], errorSpy); - expect(exitCode).toEqual(0); - - const mymodulejs = path.resolve(outDir, 'mymodule.js'); - const mymoduleSource = fs.readFileSync(mymodulejs, 'utf8'); - expect(mymoduleSource).not.toContain('__decorate'); - expect(mymoduleSource).toContain('args: [{ declarations: [] },] }'); - expect(mymoduleSource).not.toContain(`__metadata`); - expect(mymoduleSource).toContain(`import { AClass } from './aclass';`); - expect(mymoduleSource).toContain(`{ type: AClass }`); + expect(mymoduleSource).toMatch(/\/\*\* @nocollapse \*\/\s+MyComp\.ctorParameters = /); }); }); diff --git a/packages/compiler-cli/test/transformers/BUILD.bazel b/packages/compiler-cli/test/transformers/BUILD.bazel index dd82f46edc811..60965e95b4fdb 100644 --- a/packages/compiler-cli/test/transformers/BUILD.bazel +++ b/packages/compiler-cli/test/transformers/BUILD.bazel @@ -8,6 +8,7 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler-cli", + "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/test:test_utils", "//packages/compiler/test:test_utils", "//packages/core", diff --git a/packages/compiler-cli/test/transformers/downlevel_decorators_transform_spec.ts b/packages/compiler-cli/test/transformers/downlevel_decorators_transform_spec.ts new file mode 100644 index 0000000000000..964ca8079ac01 --- /dev/null +++ b/packages/compiler-cli/test/transformers/downlevel_decorators_transform_spec.ts @@ -0,0 +1,624 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {TypeScriptReflectionHost} from '../../src/ngtsc/reflection'; +import {getDownlevelDecoratorsTransform} from '../../src/transformers/downlevel_decorators_transform'; +import {MockAotContext, MockCompilerHost} from '../mocks'; + +const TEST_FILE_INPUT = '/test.ts'; +const TEST_FILE_OUTPUT = `/test.js`; +const TEST_FILE_DTS_OUTPUT = `/test.d.ts`; + +describe('downlevel decorator transform', () => { + let host: MockCompilerHost; + let context: MockAotContext; + let diagnostics: ts.Diagnostic[]; + let isClosureEnabled: boolean; + + beforeEach(() => { + diagnostics = []; + context = new MockAotContext('/', { + 'dom_globals.d.ts': ` + declare class HTMLElement {}; + declare class Document {}; + ` + }); + host = new MockCompilerHost(context); + isClosureEnabled = false; + }); + + function transform( + contents: string, compilerOptions: ts.CompilerOptions = {}, + preTransformers: ts.TransformerFactory[] = []) { + context.writeFile(TEST_FILE_INPUT, contents); + const program = ts.createProgram( + [TEST_FILE_INPUT, '/dom_globals.d.ts'], { + module: ts.ModuleKind.CommonJS, + importHelpers: true, + lib: ['dom', 'es2015'], + target: ts.ScriptTarget.ES2017, + declaration: true, + experimentalDecorators: true, + emitDecoratorMetadata: false, + ...compilerOptions + }, + host); + const testFile = program.getSourceFile(TEST_FILE_INPUT); + const typeChecker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const transformers: ts.CustomTransformers = { + before: [ + ...preTransformers, + getDownlevelDecoratorsTransform( + program.getTypeChecker(), reflectionHost, diagnostics, + /* isCore */ false, isClosureEnabled) + ] + }; + let output: string|null = null; + let dtsOutput: string|null = null; + const emitResult = program.emit( + testFile, ((fileName, outputText) => { + if (fileName === TEST_FILE_OUTPUT) { + output = outputText; + } else if (fileName === TEST_FILE_DTS_OUTPUT) { + dtsOutput = outputText; + } + }), + undefined, undefined, transformers); + diagnostics.push(...emitResult.diagnostics); + expect(output).not.toBeNull(); + return { + output: omitLeadingWhitespace(output!), + dtsOutput: dtsOutput ? omitLeadingWhitespace(dtsOutput) : null + }; + } + + it('should downlevel decorators for @Injectable decorated class', () => { + const {output} = transform(` + import {Injectable} from '@angular/core'; + + export class ClassInject {}; + + @Injectable() + export class MyService { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyService.decorators = [ + { type: core_1.Injectable } + ]; + MyService.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Directive decorated class', () => { + const {output} = transform(` + import {Directive} from '@angular/core'; + + export class ClassInject {}; + + @Directive() + export class MyDir { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Component decorated class', () => { + const {output} = transform(` + import {Component} from '@angular/core'; + + export class ClassInject {}; + + @Component({template: 'hello'}) + export class MyComp { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyComp.decorators = [ + { type: core_1.Component, args: [{ template: 'hello' },] } + ]; + MyComp.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should downlevel decorators for @Pipe decorated class', () => { + const {output} = transform(` + import {Pipe} from '@angular/core'; + + export class ClassInject {}; + + @Pipe({selector: 'hello'}) + export class MyPipe { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyPipe.decorators = [ + { type: core_1.Pipe, args: [{ selector: 'hello' },] } + ]; + MyPipe.ctorParameters = () => [ + { type: ClassInject } + ];`); + expect(output).not.toContain('tslib'); + }); + + it('should not downlevel non-Angular class decorators', () => { + const {output} = transform(` + @SomeUnknownDecorator() + export class MyClass {} + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyClass = tslib_1.__decorate([ + SomeUnknownDecorator() + ], MyClass); + `); + expect(output).not.toContain('MyClass.decorators'); + }); + + it('should downlevel Angular-decorated class member', () => { + const {output} = transform(` + import {Input} from '@angular/core'; + + export class MyDir { + @Input() disabled: boolean = false; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.propDecorators = { + disabled: [{ type: core_1.Input }] + }; + `); + expect(output).not.toContain('tslib'); + }); + + it('should not downlevel class member with unknown decorator', () => { + const {output} = transform(` + export class MyDir { + @SomeDecorator() disabled: boolean = false; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + tslib_1.__decorate([ + SomeDecorator() + ], MyDir.prototype, "disabled", void 0); + `); + expect(output).not.toContain('MyClass.propDecorators'); + }); + + // Angular is not concerned with type information for decorated class members. Instead, + // the type is omitted. This also helps with server side rendering as DOM globals which + // are used as types, do not load at runtime. https://github.com/angular/angular/issues/30586. + it('should downlevel Angular-decorated class member but not preserve type', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform(` + import {Input} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + export class MyDir { + @Input() trigger: HTMLElement; + @Input() fromOtherFile: MyOtherClass; + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.propDecorators = { + trigger: [{ type: core_1.Input }], + fromOtherFile: [{ type: core_1.Input }] + }; + `); + expect(output).not.toContain('HTMLElement'); + expect(output).not.toContain('MyOtherClass'); + }); + + it('should capture constructor type metadata with `emitDecoratorMetadata` enabled', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + @Directive() + export class MyDir { + constructor(other: MyOtherClass) {} + } + `, + {emitDecoratorMetadata: true}); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const other_file_1 = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other_file_1.MyOtherClass } + ]; + `); + }); + + it('should capture constructor type metadata with `emitDecoratorMetadata` disabled', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output, dtsOutput} = transform( + ` + import {Directive} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + @Directive() + export class MyDir { + constructor(other: MyOtherClass) {} + } + `, + {emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const other_file_1 = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other_file_1.MyOtherClass } + ]; + `); + expect(dtsOutput).toContain('import'); + }); + + it('should properly serialize constructor parameter with external qualified name type', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const {output} = transform(` + import {Directive} from '@angular/core'; + import * as externalFile from './other-file'; + + @Directive() + export class MyDir { + constructor(other: externalFile.MyOtherClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('const externalFile = require("./other-file");'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: externalFile.MyOtherClass } + ]; + `); + }); + + it('should properly serialize constructor parameter with local qualified name type', () => { + const {output} = transform(` + import {Directive} from '@angular/core'; + + namespace other { + export class OtherClass {} + }; + + @Directive() + export class MyDir { + constructor(other: other.OtherClass) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain('var other;'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: other.OtherClass } + ]; + `); + }); + + it('should properly downlevel constructor parameter decorators', () => { + const {output} = transform(` + import {Inject, Directive, DOCUMENT} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Inject(DOCUMENT) document: Document) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: Document, decorators: [{ type: core_1.Inject, args: [core_1.DOCUMENT,] }] } + ]; + `); + }); + + it('should properly downlevel constructor parameters with union type', () => { + const {output} = transform(` + import {Optional, Directive, NgZone} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Optional() ngZone: NgZone|null) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: core_1.NgZone, decorators: [{ type: core_1.Optional }] } + ]; + `); + }); + + it('should add @nocollapse if closure compiler is enabled', () => { + isClosureEnabled = true; + const {output} = transform(` + import {Directive} from '@angular/core'; + + export class ClassInject {}; + + @Directive() + export class MyDir { + constructor(v: ClassInject) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + /** @nocollapse */ + MyDir.ctorParameters = () => [ + { type: ClassInject } + ]; + `); + expect(output).not.toContain('tslib'); + }); + + it('should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` enabled.', + () => { + context.writeFile('/external.ts', ` + export class ErrorHandler {} + export class ClassInject {} + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {ErrorHandler, ClassInject} from './external'; + + @Directive() + export class MyDir { + private _errorHandler: ErrorHandler; + constructor(v: ClassInject) {} + } + `, + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: true}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).not.toContain('ErrorHandler'); + }); + + it('should not retain unused type imports due to decorator downleveling with ' + + '`emitDecoratorMetadata` disabled', + () => { + context.writeFile('/external.ts', ` + export class ErrorHandler {} + export class ClassInject {} + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {ErrorHandler, ClassInject} from './external'; + + @Directive() + export class MyDir { + private _errorHandler: ErrorHandler; + constructor(v: ClassInject) {} + } + `, + {module: ts.ModuleKind.ES2015, emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).not.toContain('ErrorHandler'); + }); + + it('should not generate invalid reference due to conflicting parameter name', () => { + context.writeFile('/external.ts', ` + export class Dep { + greet() {} + } + `); + const {output} = transform( + ` + import {Directive} from '@angular/core'; + import {Dep} from './external'; + + @Directive() + export class MyDir { + constructor(Dep: Dep) { + Dep.greet(); + } + } + `, + {emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('tslib'); + expect(output).toContain(`external_1 = require("./external");`); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: external_1.Dep } + ]; + `); + }); + + it('should be able to serialize circular constructor parameter type', () => { + const {output} = transform(` + import {Directive, Optional, Inject, SkipSelf} from '@angular/core'; + + @Directive() + export class MyDir { + constructor(@Optional() @SkipSelf() @Inject(MyDir) parentDir: MyDir|null) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: MyDir, decorators: [{ type: core_1.Optional }, { type: core_1.SkipSelf }, { type: core_1.Inject, args: [MyDir,] }] } + ]; + `); + }); + + it('should create diagnostic if property name is non-serializable', () => { + transform(` + import {Directive, ViewChild, TemplateRef} from '@angular/core'; + + @Directive() + export class MyDir { + @ViewChild(TemplateRef) ['some' + 'name']: TemplateRef|undefined; + } + `); + + expect(diagnostics.length).toBe(1); + expect(diagnostics[0].messageText as string) + .toBe(`Cannot process decorators for class element with non-analyzable name.`); + }); + + it('should not capture constructor parameter types when not resolving to a value', () => { + context.writeFile('/external.ts', ` + export interface IState {} + export type IOverlay = {hello: true}&IState; + export default interface { + hello: false; + } + `); + const {output} = transform(` + import {Directive, Inject} from '@angular/core'; + import * as angular from './external'; + import {IOverlay} from './external'; + import TypeFromDefaultImport from './external'; + + @Directive() + export class MyDir { + constructor(@Inject('$state') param: angular.IState, + @Inject('$overlay') other: IOverlay, + @Inject('$default') default: TypeFromDefaultImport) {} + } + `); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('external'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$state',] }] }, + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$overlay',] }] }, + { type: undefined, decorators: [{ type: core_1.Inject, args: ['$default',] }] } + ]; + `); + }); + + it('should allow preceding custom transformers to strip decorators', () => { + const stripAllDecoratorsTransform: ts.TransformerFactory = context => { + return (sourceFile: ts.SourceFile) => { + const visitNode = (node: ts.Node): ts.Node => { + if (ts.isClassDeclaration(node) || ts.isClassElement(node)) { + const cloned = ts.getMutableClone(node); + cloned.decorators = undefined; + return cloned; + } + return ts.visitEachChild(node, visitNode, context); + }; + return visitNode(sourceFile) as ts.SourceFile; + }; + }; + + const {output} = transform( + ` + import {Directive} from '@angular/core'; + + export class MyInjectedClass {} + + @Directive() + export class MyDir { + constructor(someToken: MyInjectedClass) {} + } + `, + {}, [stripAllDecoratorsTransform]); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('MyDir.decorators'); + expect(output).not.toContain('MyDir.ctorParameters'); + expect(output).not.toContain('tslib'); + }); +}); + +/** Template string function that can be used to dedent a given string literal. */ +export function dedent(strings: TemplateStringsArray, ...values: any[]) { + let joinedString = ''; + for (let i = 0; i < values.length; i++) { + joinedString += `${strings[i]}${values[i]}`; + } + joinedString += strings[strings.length - 1]; + return omitLeadingWhitespace(joinedString); +} + +/** Omits the leading whitespace for each line of the given text. */ +function omitLeadingWhitespace(text: string): string { + return text.replace(/^\s+/gm, ''); +} diff --git a/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md index c2baf47676113..18e924ac47295 100644 --- a/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md +++ b/packages/core/test/bundling/todo_i18n/OUTSTANDING_WORK.md @@ -7,7 +7,7 @@ - [ ] Make it work with `(keyup.Enter)`. ## Compiler -- [ ] Remove ` tslib_1.__decorate([core_1.Input(), tslib_1.__metadata("design:type", Object)], TodoComponent.prototype, "todo", void 0);` from generated output. +- [X] Remove ` tslib_1.__decorate([core_1.Input(), tslib_1.__metadata("design:type", Object)], TodoComponent.prototype, "todo", void 0);` from generated output. - [ ] Allow compilation of `@angular/common` through ivy. ## Ivy Runtime @@ -33,4 +33,4 @@ ports even after `ctrl-c`. This command kills the outstanding processes. ``` kill -9 $(ps aux | grep ibazel\\\|devserver | cut -c 17-23) -``` \ No newline at end of file +```