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'); + }); });