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 +```