diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index eb9e9fa2f0ad49..5f5b7f0ad000d5 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,23 @@ export function compile({ }; } + // Always 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, annotations for closure compiler are disabled in prodmode, + // unless the workspace is known to be `google3`. + if (!bazelOpts.es5Mode && compilerOpts.annotateForClosureCompiler === undefined) { + compilerOpts.annotateForClosureCompiler = bazelOpts.workspaceName === 'google3'; + } + + // 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 ed9853d15619bc..f31be526345299 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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uLy4uL3BhY2thZ2VzL2JhemVsL3Rlc3QvbmdfcGFja2FnZS9leGFtcGxlL2ltcG9ydHMvcHVibGljLWFwaS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7Ozs7O0dBTUc7QUFFSCxPQUFPLEVBQUMsVUFBVSxFQUFDLE1BQU0sZUFBZSxDQUFDO0FBQ3pDLE9BQU8sRUFBQyxlQUFlLEVBQUMsTUFBTSxVQUFVLENBQUM7OztBQUV6QztJQUFBLE1BQ2EsU0FBUztRQUNwQixZQUFtQixhQUE4QjtZQUE5QixrQkFBYSxHQUFiLGFBQWEsQ0FBaUI7UUFBRyxDQUFDOzs7O2dCQUZ0RCxVQUFVLFNBQUMsRUFBQyxVQUFVLEVBQUUsTUFBTSxFQUFDOzs7Z0JBRUksZUFBZTs7b0JBYm5EO0tBY0M7U0FGWSxTQUFTIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBAbGljZW5zZVxuICogQ29weXJpZ2h0IEdvb2dsZSBMTEMgQWxsIFJpZ2h0cyBSZXNlcnZlZC5cbiAqXG4gKiBVc2Ugb2YgdGhpcyBzb3VyY2UgY29kZSBpcyBnb3Zlcm5lZCBieSBhbiBNSVQtc3R5bGUgbGljZW5zZSB0aGF0IGNhbiBiZVxuICogZm91bmQgaW4gdGhlIExJQ0VOU0UgZmlsZSBhdCBodHRwczovL2FuZ3VsYXIuaW8vbGljZW5zZVxuICovXG5cbmltcG9ydCB7SW5qZWN0YWJsZX0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XG5pbXBvcnQge015U2Vjb25kU2VydmljZX0gZnJvbSAnLi9zZWNvbmQnO1xuXG5ASW5qZWN0YWJsZSh7cHJvdmlkZWRJbjogJ3Jvb3QnfSlcbmV4cG9ydCBjbGFzcyBNeVNlcnZpY2Uge1xuICBjb25zdHJ1Y3RvcihwdWJsaWMgc2Vjb25kU2VydmljZTogTXlTZWNvbmRTZXJ2aWNlKSB7fVxufVxuIl19 --- 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 fa0e14e6e0411a..2ad66dfa6aff32 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/integrationtest/tsconfig-build.json b/packages/compiler-cli/integrationtest/tsconfig-build.json index f9c08623f901b2..4bb508fec73aa9 100644 --- a/packages/compiler-cli/integrationtest/tsconfig-build.json +++ b/packages/compiler-cli/integrationtest/tsconfig-build.json @@ -12,6 +12,7 @@ "baseUrl": ".", "declaration": true, "experimentalDecorators": true, + "emitDecoratorMetadata": true, "lib": ["es6", "dom"], "moduleResolution": "node", "noImplicitAny": true, diff --git a/packages/compiler-cli/src/main.ts b/packages/compiler-cli/src/main.ts index a629759a4b8248..0c8eb8ddb899f9 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 0f9f29271a8bee..91af7253655dd1 100644 --- a/packages/compiler-cli/src/ngtsc/core/src/compiler.ts +++ b/packages/compiler-cli/src/ngtsc/core/src/compiler.ts @@ -754,7 +754,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/transformers/downlevel_decorators_transform.ts b/packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts new file mode 100644 index 00000000000000..56e37a07970bd0 --- /dev/null +++ b/packages/compiler-cli/src/transformers/downlevel_decorators_transform.ts @@ -0,0 +1,551 @@ +/** + * @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/src/host'; + +/** + * 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'); +} + +function isImportSymbolDeclaration(node: ts.Node): node is ts.ImportSpecifier|ts.NamespaceImport| + ts.ImportClause { + return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node); +} + +/* + ##################################################################### + 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 made this work properly with + respect to the flag by not emitting design type metadata if that flag is set to false. + + 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); + // initializer.elements.hasTrailingComma = true; + const prop = ts.createProperty(undefined, [modifier], 'decorators', undefined, type, initializer); + // 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 prop; +} + +/** + * 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 diagnostics List which will be populated with diagnostics if any. + * @param host Reflection host that is used for determining decorators. + * @param isCore Whether the current TypeScript program is for the `@angular/core` package. + * @param emitTypeMetadata Whether type information should be collected. + * @param isClosureCompilerEnabled Whether closure annotations need to be added where needed. + */ +export function getDownlevelDecoratorsTransform( + typeChecker: ts.TypeChecker, diagnostics: ts.Diagnostic[], host: ReflectionHost, + isCore: boolean, emitTypeMetadata: boolean, + isClosureCompilerEnabled: boolean): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + /** + * 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 { + if (ts.isQualifiedName(name)) { + const containerExpr = entityNameToExpression(name.left); + if (containerExpr === undefined) { + return undefined; + } + return ts.createPropertyAccess(containerExpr, name.right); + } + 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)) { + return undefined; + } + 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 || element.name.kind !== ts.SyntaxKind.Identifier) { + // 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 (emitTypeMetadata && 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; + } + + function decoratorDownlevelVisitor(node: ts.Node): ts.Node { + switch (node.kind) { + case ts.SyntaxKind.SourceFile: { + return ts.visitEachChild(node, decoratorDownlevelVisitor, context); + } + case ts.SyntaxKind.ClassDeclaration: { + return transformClassDeclaration(node as ts.ClassDeclaration); + } + default: + return ts.visitEachChild(node, decoratorDownlevelVisitor, context); + } + } + + return (sf: ts.SourceFile) => decoratorDownlevelVisitor(sf) as ts.SourceFile; + }; +} diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index dbc3dcdb709f75..6fd76e3e095594 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/src/typescript'; 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, @@ -204,7 +197,7 @@ class AngularCompilerProgram implements Program { getTsSemanticDiagnostics(sourceFile?: ts.SourceFile, cancellationToken?: ts.CancellationToken): ReadonlyArray { const sourceFiles = sourceFile ? [sourceFile] : this.tsProgram.getSourceFiles(); - let diags: ts.Diagnostic[] = []; + let diags: ts.Diagnostic[] = [...this._transformTsDiagnostics]; sourceFiles.forEach(sf => { if (!GENERATED_FILES.test(sf.fileName)) { diags.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken)); @@ -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,24 @@ 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 emitTypeMetadata = this.options.emitDecoratorMetadata === true; + const typeChecker = this.getTsProgram().getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + beforeTs.push(getDownlevelDecoratorsTransform( + typeChecker, this._transformTsDiagnostics, reflectionHost, this.isCompilingAngularCore, + emitTypeMetadata, 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 03f480e7075409..00000000000000 --- 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 8eee29092b6d37..c4de996f16bc23 100644 --- a/packages/compiler-cli/test/ngc_spec.ts +++ b/packages/compiler-cli/test/ngc_spec.ts @@ -557,8 +557,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', () => { @@ -623,8 +621,6 @@ describe('ngc transformer command-line', () => { }); 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": { @@ -654,9 +650,45 @@ describe('ngc transformer command-line', () => { 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).not.toContain(`import { AClass } from './aclass';`); + expect(mymoduleSource).not.toContain(`{ type: AClass }`); }); + + it('should add metadata as static fields with type information if ' + + '`emitDecoratorMetadata` is enabled', + () => { + writeConfig(`{ + "extends": "./tsconfig-base.json", + "compilerOptions": { + "emitDecoratorMetadata": true + }, + "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 }`); + }); }); it('should not rewrite imports when annotating with closure', () => { diff --git a/packages/compiler-cli/test/transformers/BUILD.bazel b/packages/compiler-cli/test/transformers/BUILD.bazel index dd82f46edc8113..60965e95b4fdbc 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 00000000000000..cf25d6574de5b7 --- /dev/null +++ b/packages/compiler-cli/test/transformers/downlevel_decorators_transform_spec.ts @@ -0,0 +1,543 @@ +/** + * @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/src/typescript'; +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`; + +describe('downlevel decorator transform', () => { + let host: MockCompilerHost; + let context: MockAotContext; + let diagnostics: ts.Diagnostic[]; + let isClosureEnabled: boolean; + + beforeEach(() => { + diagnostics = []; + context = new MockAotContext('/', {'test.ts': ''}); + 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], { + module: ts.ModuleKind.CommonJS, + importHelpers: true, + target: ts.ScriptTarget.ES2017, + experimentalDecorators: true, + emitDecoratorMetadata: true, + ...compilerOptions + }, + host); + const testFile = program.getSourceFile(TEST_FILE_INPUT); + const typeChecker = program.getTypeChecker(); + const reflectionHost = new TypeScriptReflectionHost(typeChecker); + const options = program.getCompilerOptions(); + const transformers: ts.CustomTransformers = { + before: [ + ...preTransformers, + getDownlevelDecoratorsTransform( + program.getTypeChecker(), diagnostics, reflectionHost, + /* isCore */ false, options.emitDecoratorMetadata === true, isClosureEnabled) + ] + }; + let testOutput: string|null = null; + const emitResult = program.emit( + testFile, ((fileName, output) => { + if (fileName === TEST_FILE_OUTPUT) { + testOutput = output; + } + }), + undefined, undefined, transformers); + diagnostics.push(...emitResult.diagnostics); + expect(testOutput).not.toBeNull(); + return omitLeadingWhitespace(testOutput!); + } + + 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(), + tslib_1.__metadata("design:type", Boolean) + ], 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 not capture constructor type metadata with `emitDecoratorMetadata` disabled', () => { + context.writeFile('/other-file.ts', `export class MyOtherClass {}`); + const output = transform( + ` + import {Directive, Optional} from '@angular/core'; + import {MyOtherClass} from './other-file'; + + @Directive() + export class MyDir { + constructor(@Optional() other: MyOtherClass) {} + } + `, + {emitDecoratorMetadata: false}); + + expect(diagnostics.length).toBe(0); + expect(output).not.toContain('other-file'); + expect(output).not.toContain('tslib'); + expect(output).toContain(dedent` + MyDir.decorators = [ + { type: core_1.Directive } + ]; + MyDir.ctorParameters = () => [ + { type: undefined, decorators: [{ type: core_1.Optional }] } + ]; + `); + }); + + 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: undefined, 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 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 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 c2baf476761132..18e924ac47295e 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 +```