From c016066d9bc9c1e2f2670d4bb9e77c942f8b87a1 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 1 Nov 2018 08:21:07 +0000 Subject: [PATCH] fix(ivy): ngcc should not break lifecycle hooks (#26856) Previously the ivy definition calls we going directly after the class constructor function But this meant that the lifecycle hooks attached to the prototype were ignored by the ngtsc compiler. Now the definitions are written to the end of the IIFE block, just before the return statement. Closes #26849 PR Close #26856 --- .../src/ngcc/src/packages/transformer.ts | 7 +- .../src/ngcc/src/rendering/esm5_renderer.ts | 44 +++++++++++ .../ngcc/test/rendering/esm5_renderer_spec.ts | 73 +++++++++++++++---- 3 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts diff --git a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts index b799970903..53d2a18def 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts @@ -15,6 +15,7 @@ import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; import {NgccReflectionHost} from '../host/ngcc_host'; +import {Esm5Renderer} from '../rendering/esm5_renderer'; import {EsmRenderer} from '../rendering/esm_renderer'; import {FileInfo, Renderer} from '../rendering/renderer'; @@ -128,11 +129,13 @@ export class Transformer { rewriteCoreImportsTo: ts.SourceFile|null, transformDts: boolean): Renderer { switch (format) { case 'esm2015': - case 'esm5': case 'fesm2015': - case 'fesm5': return new EsmRenderer( host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, transformDts); + case 'esm5': + case 'fesm5': + return new Esm5Renderer( + host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, transformDts); default: throw new Error(`Renderer for "${format}" not yet implemented.`); } diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts new file mode 100644 index 0000000000..3d8c70e396 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright Google Inc. 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 MagicString from 'magic-string'; +import {NgccReflectionHost} from '../host/ngcc_host'; +import {CompiledClass} from '../analysis/decoration_analyzer'; +import {EsmRenderer} from './esm_renderer'; + +export class Esm5Renderer extends EsmRenderer { + constructor( + protected host: NgccReflectionHost, protected isCore: boolean, + protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, + protected targetPath: string, transformDts: boolean) { + super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath, transformDts); + } + + /** + * Add the definitions to each decorated class + */ + addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void { + const classSymbol = this.host.getClassSymbol(compiledClass.declaration); + if (!classSymbol) { + throw new Error( + `Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); + } + const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent; + if (!parent || !ts.isBlock(parent)) { + throw new Error( + `Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); + } + const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement)); + if (!returnStatement) { + throw new Error( + `Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`); + } + const insertionPoint = returnStatement.getFullStart(); + output.appendLeft(insertionPoint, '\n' + definitions); + } +} diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts index 20d7f8a9ec..3cad53c8fc 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts @@ -7,11 +7,11 @@ */ import * as ts from 'typescript'; import MagicString from 'magic-string'; -import {makeProgram} from '../helpers/utils'; +import {makeProgram, getDeclaration} from '../helpers/utils'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; -import {EsmRenderer} from '../../src/rendering/esm_renderer'; +import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; function setup(file: {name: string, contents: string}) { const program = makeProgram(file); @@ -20,7 +20,7 @@ function setup(file: {name: string, contents: string}) { const decorationAnalyses = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); - const renderer = new EsmRenderer(host, false, null, '', '', false); + const renderer = new Esm5Renderer(host, false, null, '', '', false); return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses}; } @@ -35,6 +35,9 @@ var A = (function() { { type: Directive, args: [{ selector: '[a]' }] }, { type: OtherA } ]; + A.prototype.ngDoCheck = function() { + // + }; return A; }()); @@ -55,6 +58,15 @@ var C = (function() { return C; }()); +function NoIife() {} + +var BadIife = (function() { + function BadIife() {} + BadIife.decorators = [ + { type: Directive, args: [{ selector: '[c]' }] }, + ]; +}()); + var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { @@ -68,7 +80,7 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { return Promise.resolve(new R3NgModuleFactory(moduleType)); } // Some other content -export {A, B, C};` +export {A, B, C, NoIife, BadIife};` }; const PROGRAM_DECORATE_HELPER = { @@ -179,19 +191,52 @@ var A = (function() {`); }); describe('addDefinitions', () => { - it('should insert the definitions directly after the class declaration', () => { - const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); - const output = new MagicString(PROGRAM.contents); - const compiledClass = - decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; - renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); - expect(output.toString()).toContain(` - function A() {} + it('should insert the definitions directly before the return statement of the class IIFE', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()).toContain(` + A.prototype.ngDoCheck = function() { + // + }; SOME DEFINITION TEXT - A.decorators = [ + return A; `); - }); + }); + it('should error if the compiledClass is not valid', () => { + const {renderer, host, sourceFile, program} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + + const badSymbolDeclaration = + getDeclaration(program, sourceFile.fileName, 'A', ts.isVariableDeclaration); + const badSymbol: any = {name: 'BadSymbol', declaration: badSymbolDeclaration}; + const hostSpy = spyOn(host, 'getClassSymbol').and.returnValue(null); + expect(() => renderer.addDefinitions(output, badSymbol, 'SOME DEFINITION TEXT')) + .toThrowError('Compiled class does not have a valid symbol: BadSymbol in /some/file.js'); + + + const noIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; + hostSpy.and.returnValue({valueDeclaration: noIifeDeclaration}); + expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); + + const badIifeWrapper: any = + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); + const badIifeDeclaration = + badIifeWrapper.initializer.expression.expression.body.statements[0]; + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; + hostSpy.and.returnValue({valueDeclaration: badIifeDeclaration}); + expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); + }); });