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
This commit is contained in:
Pete Bacon Darwin 2018-11-01 08:21:07 +00:00 committed by Kara Erickson
parent beabfb7960
commit c016066d9b
3 changed files with 108 additions and 16 deletions

View File

@ -15,6 +15,7 @@ import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host'; import {Esm5ReflectionHost} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {Esm5Renderer} from '../rendering/esm5_renderer';
import {EsmRenderer} from '../rendering/esm_renderer'; import {EsmRenderer} from '../rendering/esm_renderer';
import {FileInfo, Renderer} from '../rendering/renderer'; import {FileInfo, Renderer} from '../rendering/renderer';
@ -128,11 +129,13 @@ export class Transformer {
rewriteCoreImportsTo: ts.SourceFile|null, transformDts: boolean): Renderer { rewriteCoreImportsTo: ts.SourceFile|null, transformDts: boolean): Renderer {
switch (format) { switch (format) {
case 'esm2015': case 'esm2015':
case 'esm5':
case 'fesm2015': case 'fesm2015':
case 'fesm5':
return new EsmRenderer( return new EsmRenderer(
host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, transformDts); host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, transformDts);
case 'esm5':
case 'fesm5':
return new Esm5Renderer(
host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, transformDts);
default: default:
throw new Error(`Renderer for "${format}" not yet implemented.`); throw new Error(`Renderer for "${format}" not yet implemented.`);
} }

View File

@ -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);
}
}

View File

@ -7,11 +7,11 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {makeProgram} from '../helpers/utils'; import {makeProgram, getDeclaration} from '../helpers/utils';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm5ReflectionHost} from '../../src/host/esm5_host'; 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}) { function setup(file: {name: string, contents: string}) {
const program = makeProgram(file); const program = makeProgram(file);
@ -20,7 +20,7 @@ function setup(file: {name: string, contents: string}) {
const decorationAnalyses = const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).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}; return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses};
} }
@ -35,6 +35,9 @@ var A = (function() {
{ type: Directive, args: [{ selector: '[a]' }] }, { type: Directive, args: [{ selector: '[a]' }] },
{ type: OtherA } { type: OtherA }
]; ];
A.prototype.ngDoCheck = function() {
//
};
return A; return A;
}()); }());
@ -55,6 +58,15 @@ var C = (function() {
return C; return C;
}()); }());
function NoIife() {}
var BadIife = (function() {
function BadIife() {}
BadIife.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
}());
var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;
var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;
function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {
@ -68,7 +80,7 @@ function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
return Promise.resolve(new R3NgModuleFactory(moduleType)); return Promise.resolve(new R3NgModuleFactory(moduleType));
} }
// Some other content // Some other content
export {A, B, C};` export {A, B, C, NoIife, BadIife};`
}; };
const PROGRAM_DECORATE_HELPER = { const PROGRAM_DECORATE_HELPER = {
@ -179,19 +191,52 @@ var A = (function() {`);
}); });
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const compiledClass = const output = new MagicString(PROGRAM.contents);
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; const compiledClass =
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
expect(output.toString()).toContain(` renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
function A() {} expect(output.toString()).toContain(`
A.prototype.ngDoCheck = function() {
//
};
SOME DEFINITION TEXT 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');
});
}); });