diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts index 9714a3714b..831a239c9d 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts @@ -54,11 +54,11 @@ export class Esm2015Renderer extends Renderer { if (ts.isArrayLiteralExpression(containerNode)) { const items = containerNode.elements; if (items.length === nodesToRemove.length) { - // remove any trailing semi-colon - const end = (output.slice(containerNode.getEnd(), containerNode.getEnd() + 1) === ';') ? - containerNode.getEnd() + 1 : - containerNode.getEnd(); - output.remove(containerNode.parent !.getFullStart(), end); + // Remove the entire statement + const statement = findStatement(containerNode); + if (statement) { + output.remove(statement.getFullStart(), statement.getEnd()); + } } else { nodesToRemove.forEach(node => { // remove any trailing comma @@ -82,3 +82,13 @@ export class Esm2015Renderer extends Renderer { }); } } + +function findStatement(node: ts.Node) { + while (node) { + if (ts.isExpressionStatement(node)) { + return node; + } + node = node.parent; + } + return undefined; +} diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index 398df52a94..b66c4eaae3 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -62,6 +62,44 @@ function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { // Some other content` }; +const PROGRAM_DECORATE_HELPER = { + name: 'some/file.js', + contents: ` +import * as tslib_1 from "tslib"; +var D_1; +/* A copyright notice */ +import { Directive } from '@angular/core'; +const OtherA = () => (node) => { }; +const OtherB = () => (node) => { }; +let A = class A { +}; +A = tslib_1.__decorate([ + Directive({ selector: '[a]' }), + OtherA() +], A); +export { A }; +let B = class B { +}; +B = tslib_1.__decorate([ + OtherB(), + Directive({ selector: '[b]' }) +], B); +export { B }; +let C = class C { +}; +C = tslib_1.__decorate([ + Directive({ selector: '[c]' }) +], C); +export { C }; +let D = D_1 = class D { +}; +D = D_1 = tslib_1.__decorate([ + Directive({ selector: '[d]', providers: [D_1] }) +], D); +export { D }; +// Some other content` +}; + describe('Esm2015Renderer', () => { describe('addImports', () => { @@ -136,61 +174,121 @@ A.decorators = [ describe('removeDecorators', () => { + describe('[static property declaration]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', + () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM); + const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); + const output = new MagicString(PROGRAM.contents); + const analyzedClass = analyzedFile.analyzedClasses[0]; + const decorator = analyzedClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM); + const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); + const output = new MagicString(PROGRAM.contents); + const analyzedClass = analyzedFile.analyzedClasses[1]; + const decorator = analyzedClass.decorators[1]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM); + const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); + const output = new MagicString(PROGRAM.contents); + const analyzedClass = analyzedFile.analyzedClasses[2]; + const decorator = analyzedClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).not.toContain(`C.decorators = [`); + }); + }); + }); + + describe('[__decorate declarations]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {analyzer, parser, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); - const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; - const decorator = analyzedClass.decorators[0]; + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'A') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); }); - it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {analyzer, parser, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); - const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[1]; - const decorator = analyzedClass.decorators[1]; + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'B') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()) - .not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); }); it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {analyzer, parser, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(parser, analyzer, program.getSourceFile(PROGRAM.name) !); - const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[2]; - const decorator = analyzedClass.decorators[0]; + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'C') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`); - expect(output.toString()).toContain(`{ type: OtherA }`); - expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`); - expect(output.toString()).toContain(`{ type: OtherB }`); - expect(output.toString()).not.toContain(`C.decorators = [ - { type: Directive, args: [{ selector: '[c]' }] }, -];`); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`); }); - }); }); 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 7de4e0ab24..bbc5841149 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 @@ -74,6 +74,57 @@ function compileNgModuleFactory__POST_NGCC__(injector, options, moduleType) { export {A, B, C};` }; +const PROGRAM_DECORATE_HELPER = { + name: 'some/file.js', + contents: ` +import * as tslib_1 from "tslib"; +/* A copyright notice */ +import { Directive } from '@angular/core'; +var OtherA = function () { return function (node) { }; }; +var OtherB = function () { return function (node) { }; }; +var A = /** @class */ (function () { + function A() { + } + A = tslib_1.__decorate([ + Directive({ selector: '[a]' }), + OtherA() + ], A); + return A; +}()); +export { A }; +var B = /** @class */ (function () { + function B() { + } + B = tslib_1.__decorate([ + OtherB(), + Directive({ selector: '[b]' }) + ], B); + return B; +}()); +export { B }; +var C = /** @class */ (function () { + function C() { + } + C = tslib_1.__decorate([ + Directive({ selector: '[c]' }) + ], C); + return C; +}()); +export { C }; +var D = /** @class */ (function () { + function D() { + } + D_1 = D; + var D_1; + D = D_1 = tslib_1.__decorate([ + Directive({ selector: '[d]', providers: [D_1] }) + ], D); + return D; +}()); +export { D }; +// Some other content` +}; + describe('Esm5Renderer', () => { describe('addImports', () => { @@ -205,4 +256,62 @@ SOME DEFINITION TEXT }); }); + + describe('[__decorate declarations]', () => { + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'A') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'B') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`Directive({ selector: '[c]' })`); + }); + + + it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', + () => { + const {analyzer, parser, program, renderer} = setup(PROGRAM_DECORATE_HELPER); + const analyzedFile = + analyze(parser, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'C') !; + const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); });