From f6aa60c03cc02992b1e63af80b21b10513fe3194 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 28 Apr 2019 20:48:35 +0100 Subject: [PATCH] feat(ivy): ngcc - implement `UmdRenderer` (#25445) PR Close #25445 --- .../ngcc/src/rendering/esm_renderer.ts | 6 +- .../ngcc/src/rendering/renderer.ts | 35 +- .../ngcc/src/rendering/umd_renderer.ts | 207 ++++++++ .../test/rendering/esm2015_renderer_spec.ts | 40 +- .../ngcc/test/rendering/esm5_renderer_spec.ts | 52 +- .../ngcc/test/rendering/renderer_spec.ts | 44 +- .../ngcc/test/rendering/umd_renderer_spec.ts | 490 ++++++++++++++++++ 7 files changed, 812 insertions(+), 62 deletions(-) create mode 100644 packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts create mode 100644 packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts diff --git a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts index 607595449f..3b2be815d3 100644 --- a/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts @@ -9,7 +9,7 @@ import MagicString from 'magic-string'; import * as ts from 'typescript'; import {PathSegment, AbsoluteFsPath} from '../../../src/ngtsc/path'; import {isDtsPath} from '../../../src/ngtsc/util/src/typescript'; -import {Import} from '../../../src/ngtsc/translator'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; import {CompiledClass} from '../analysis/decoration_analyzer'; import {ExportInfo} from '../analysis/private_declarations_analyzer'; import {FileSystem} from '../file_system/file_system'; @@ -35,7 +35,9 @@ export class EsmRenderer extends Renderer { output.appendLeft(insertionPoint, renderedImports); } - addExports(output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void { + addExports( + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { exports.forEach(e => { let exportFrom = ''; const isDtsFile = isDtsPath(entryPointBasePath); diff --git a/packages/compiler-cli/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/ngcc/src/rendering/renderer.ts index eb69584086..f2dbf5b276 100644 --- a/packages/compiler-cli/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/ngcc/src/rendering/renderer.ts @@ -127,6 +127,7 @@ export abstract class Renderer { sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined, switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] { + const isEntryPoint = sourceFile === this.bundle.src.file; const input = this.extractSourceMap(sourceFile); const outputText = new MagicString(input.source); @@ -135,11 +136,11 @@ export abstract class Renderer { outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); } - if (compiledFile) { - const importManager = new ImportManager( - this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), - IMPORT_PREFIX); + const importManager = new ImportManager( + this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlatCore), + IMPORT_PREFIX); + if (compiledFile) { // TODO: remove constructor param metadata and property decorators (we need info from the // handlers to do this) const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses); @@ -154,19 +155,24 @@ export abstract class Renderer { outputText, renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager), compiledFile.sourceFile); - - this.addImports( - outputText, importManager.getAllImports(compiledFile.sourceFile.fileName), - compiledFile.sourceFile); } // Add exports to the entry-point file - if (sourceFile === this.bundle.src.file) { + if (isEntryPoint) { const entryPointBasePath = stripExtension(this.bundle.src.path); - this.addExports(outputText, entryPointBasePath, privateDeclarationsAnalyses); + this.addExports( + outputText, entryPointBasePath, privateDeclarationsAnalyses, importManager, sourceFile); } - return this.renderSourceAndMap(sourceFile, input, outputText); + if (isEntryPoint || compiledFile) { + this.addImports(outputText, importManager.getAllImports(sourceFile.fileName), sourceFile); + } + + if (compiledFile || switchMarkerAnalysis || isEntryPoint) { + return this.renderSourceAndMap(sourceFile, input, outputText); + } else { + return []; + } } renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] { @@ -189,7 +195,9 @@ export abstract class Renderer { this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager); this.addImports(outputText, importManager.getAllImports(dtsFile.fileName), dtsFile); - this.addExports(outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports); + this.addExports( + outputText, AbsoluteFsPath.fromSourceFile(dtsFile), renderInfo.privateExports, + importManager, dtsFile); return this.renderSourceAndMap(dtsFile, input, outputText); @@ -251,7 +259,8 @@ export abstract class Renderer { void; protected abstract addImports(output: MagicString, imports: Import[], sf: ts.SourceFile): void; protected abstract addExports( - output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[]): void; + output: MagicString, entryPointBasePath: AbsoluteFsPath, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void; protected abstract addDefinitions( output: MagicString, compiledClass: CompiledClass, definitions: string): void; protected abstract removeDecorators( diff --git a/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts b/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts new file mode 100644 index 0000000000..e3ccfff46f --- /dev/null +++ b/packages/compiler-cli/ngcc/src/rendering/umd_renderer.ts @@ -0,0 +1,207 @@ +/** + * @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 {dirname, relative} from 'canonical-path'; +import * as ts from 'typescript'; +import MagicString from 'magic-string'; +import {Import, ImportManager} from '../../../src/ngtsc/translator'; +import {ExportInfo} from '../analysis/private_declarations_analyzer'; +import {FileSystem} from '../file_system/file_system'; +import {UmdReflectionHost} from '../host/umd_host'; +import {Logger} from '../logging/logger'; +import {EntryPointBundle} from '../packages/entry_point_bundle'; +import {Esm5Renderer} from './esm5_renderer'; +import {stripExtension} from './renderer'; + +type CommonJsConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; +type AmdConditional = ts.ConditionalExpression & {whenTrue: ts.CallExpression}; + +export class UmdRenderer extends Esm5Renderer { + constructor( + fs: FileSystem, logger: Logger, protected umdHost: UmdReflectionHost, isCore: boolean, + bundle: EntryPointBundle) { + super(fs, logger, umdHost, isCore, bundle); + } + + /** + * Add the imports at the top of the file + */ + addImports(output: MagicString, imports: Import[], file: ts.SourceFile): void { + // Assume there is only one UMD module in the file + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + + const wrapperFunction = umdModule.wrapperFn; + + // We need to add new `require()` calls for each import in the CommonJS initializer + renderCommonJsDependencies(output, wrapperFunction, imports); + renderAmdDependencies(output, wrapperFunction, imports); + renderGlobalDependencies(output, wrapperFunction, imports); + renderFactoryParameters(output, wrapperFunction, imports); + } + + addExports( + output: MagicString, entryPointBasePath: string, exports: ExportInfo[], + importManager: ImportManager, file: ts.SourceFile): void { + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const lastStatement = + factoryFunction.body.statements[factoryFunction.body.statements.length - 1]; + const insertionPoint = + lastStatement ? lastStatement.getEnd() : factoryFunction.body.getEnd() - 1; + exports.forEach(e => { + const basePath = stripExtension(e.from); + const relativePath = './' + relative(dirname(entryPointBasePath), basePath); + const namedImport = entryPointBasePath !== basePath ? + importManager.generateNamedImport(relativePath, e.identifier) : + {symbol: e.identifier, moduleImport: null}; + const importNamespace = namedImport.moduleImport ? `${namedImport.moduleImport}.` : ''; + const exportStr = `\nexports.${e.identifier} = ${importNamespace}${namedImport.symbol};`; + output.appendRight(insertionPoint, exportStr); + }); + } + + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const umdModule = this.umdHost.getUmdModule(file); + if (!umdModule) { + return; + } + const factoryFunction = umdModule.factoryFn; + const firstStatement = factoryFunction.body.statements[0]; + const insertionPoint = + firstStatement ? firstStatement.getStart() : factoryFunction.body.getStart() + 1; + output.appendLeft(insertionPoint, '\n' + constants + '\n'); + } +} + +function renderCommonJsDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isCommonJSConditional); + if (!conditional) { + return; + } + const factoryCall = conditional.whenTrue; + const injectionPoint = factoryCall.getEnd() - + 1; // Backup one char to account for the closing parenthesis on the call + imports.forEach(i => output.appendLeft(injectionPoint, `,require('${i.specifier}')`)); +} + +function renderAmdDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const conditional = find(wrapperFunction.body.statements[0], isAmdConditional); + if (!conditional) { + return; + } + const dependencyArray = conditional.whenTrue.arguments[1]; + if (!dependencyArray || !ts.isArrayLiteralExpression(dependencyArray)) { + return; + } + const injectionPoint = dependencyArray.getEnd() - + 1; // Backup one char to account for the closing square bracket on the array + imports.forEach(i => output.appendLeft(injectionPoint, `,'${i.specifier}'`)); +} + +function renderGlobalDependencies( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const globalFactoryCall = find(wrapperFunction.body.statements[0], isGlobalFactoryCall); + if (!globalFactoryCall) { + return; + } + const injectionPoint = globalFactoryCall.getEnd() - + 1; // Backup one char to account for the closing parenthesis on the call + imports.forEach(i => output.appendLeft(injectionPoint, `,global.${getGlobalIdentifier(i)}`)); +} + +function renderFactoryParameters( + output: MagicString, wrapperFunction: ts.FunctionExpression, imports: Import[]) { + const wrapperCall = wrapperFunction.parent as ts.CallExpression; + const secondArgument = wrapperCall.arguments[1]; + if (!secondArgument) { + return; + } + + // Be resilient to the factory being inside parentheses + const factoryFunction = + ts.isParenthesizedExpression(secondArgument) ? secondArgument.expression : secondArgument; + if (!ts.isFunctionExpression(factoryFunction)) { + return; + } + const parameters = factoryFunction.parameters; + const injectionPoint = parameters[parameters.length - 1].getEnd(); + imports.forEach(i => output.appendLeft(injectionPoint, `,${i.qualifier}`)); +} + +function isCommonJSConditional(value: ts.Node): value is CommonJsConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'exports', 'module'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'factory'; +} + +function isAmdConditional(value: ts.Node): value is AmdConditional { + if (!ts.isConditionalExpression(value)) { + return false; + } + if (!ts.isBinaryExpression(value.condition) || + value.condition.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken) { + return false; + } + if (!oneOfBinaryConditions(value.condition, (exp) => isTypeOf(exp, 'define'))) { + return false; + } + if (!ts.isCallExpression(value.whenTrue) || !ts.isIdentifier(value.whenTrue.expression)) { + return false; + } + return value.whenTrue.expression.text === 'define'; +} + +function isGlobalFactoryCall(value: ts.Node): value is ts.CallExpression { + if (ts.isCallExpression(value) && !!value.parent) { + // Be resilient to the value being inside parentheses + const expression = ts.isParenthesizedExpression(value.parent) ? value.parent : value; + return !!expression.parent && ts.isConditionalExpression(expression.parent) && + expression.parent.whenFalse === expression; + } else { + return false; + } +} + +function getGlobalIdentifier(i: Import) { + return i.specifier.replace('@angular/', 'ng.').replace(/^\//, ''); +} + +function find(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T|undefined { + return test(node) ? node : node.forEachChild(child => find(child, test)); +} + +function oneOfBinaryConditions( + node: ts.BinaryExpression, test: (expression: ts.Expression) => boolean) { + return test(node.left) || test(node.right); +} + +function isTypeOf(node: ts.Expression, ...types: string[]): boolean { + return ts.isBinaryExpression(node) && ts.isTypeOfExpression(node.left) && + ts.isIdentifier(node.left.expression) && types.indexOf(node.left.expression.text) !== -1; +} diff --git a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts index 34ba2583bf..df75d709b1 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -7,10 +7,13 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {EsmRenderer} from '../../src/rendering/esm_renderer'; import {makeTestEntryPointBundle} from '../helpers/utils'; @@ -32,10 +35,11 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new EsmRenderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager, }; } @@ -136,14 +140,17 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); expect(output.toString()).toContain(` // Some other content export {ComponentA1} from './a'; @@ -153,14 +160,17 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); expect(outputString).not.toContain(`{eComponentB as ComponentB}`); diff --git a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts index 21e0153575..fc57b1e653 100644 --- a/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts @@ -7,10 +7,13 @@ */ import MagicString from 'magic-string'; import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {ImportManager} from '../../../src/ngtsc/translator'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../../src/constants'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils'; @@ -33,10 +36,11 @@ function setup(file: {name: AbsoluteFsPath, contents: string}) { .analyzeProgram(); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program); const renderer = new Esm5Renderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), IMPORT_PREFIX); return { host, program: bundle.src.program, - sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses + sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses, importManager }; } @@ -87,8 +91,8 @@ var BadIife = (function() { var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { - const compilerFactory = injector.get(CompilerFactory); - const compiler = compilerFactory.createCompiler([options]); + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); return compiler.compileModuleAsync(moduleType); } @@ -174,14 +178,17 @@ import * as i1 from '@angular/common';`); describe('addExports', () => { it('should insert the given exports at the end of the source file', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, - {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), dtsFrom: _('/some/a.d.ts'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), dtsFrom: _('/some/foo/b.d.ts'), identifier: 'ComponentB'}, + {from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); expect(output.toString()).toContain(` export {A, B, C, NoIife, BadIife}; export {ComponentA1} from './a'; @@ -191,14 +198,17 @@ export {TopLevelComponent};`); }); it('should not insert alias exports in js output', () => { - const {renderer} = setup(PROGRAM); + const {importManager, renderer, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addExports(output, _(PROGRAM.name.replace(/\.js$/, '')), [ - {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, - {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, - {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, - {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, - ]); + renderer.addExports( + output, _(PROGRAM.name.replace(/\.js$/, '')), + [ + {from: _('/some/a.js'), alias: _('eComponentA1'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: _('eComponentA2'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: _('eComponentB'), identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); const outputString = output.toString(); expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`); expect(outputString).not.toContain(`{eComponentB as ComponentB}`); @@ -214,11 +224,11 @@ export {TopLevelComponent};`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); @@ -229,13 +239,13 @@ var A = (function() {`); throw new Error(`Could not find source file`); } const output = new MagicString(PROGRAM.contents); - renderer.addConstants(output, 'const x = 3;', file); + renderer.addConstants(output, 'var x = 3;', file); renderer.addImports(output, [{specifier: '@angular/core', qualifier: 'i0'}], file); expect(output.toString()).toContain(` import {Directive} from '@angular/core'; import * as i0 from '@angular/core'; -const x = 3; +var x = 3; var A = (function() {`); }); }); diff --git a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts index cac283fccd..81b5f97575 100644 --- a/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts @@ -13,7 +13,7 @@ import {Import} from '../../../src/ngtsc/translator'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer'; -import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer'; +import {PrivateDeclarationsAnalyzer, ExportInfo} from '../../src/analysis/private_declarations_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer'; @@ -35,10 +35,7 @@ class TestRenderer extends Renderer { addImports(output: MagicString, imports: Import[], sf: ts.SourceFile) { output.prepend('\n// ADD IMPORTS\n'); } - addExports(output: MagicString, baseEntryPointPath: string, exports: { - identifier: string, - from: string - }[]) { + addExports(output: MagicString, baseEntryPointPath: string, exports: ExportInfo[]) { output.prepend('\n// ADD EXPORTS\n'); } addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { @@ -76,8 +73,10 @@ function createTestRenderer( const privateDeclarationsAnalyses = new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program); const renderer = new TestRenderer(fs, logger, host, isCore, bundle); + spyOn(renderer, 'addExports').and.callThrough(); spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough(); + spyOn(renderer, 'addConstants').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); return {renderer, @@ -117,9 +116,17 @@ describe('Renderer', () => { 'sourcesContent': [INPUT_PROGRAM.contents] }); - const RENDERED_CONTENTS = - `\n// ADD EXPORTS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n\n// REMOVE DECORATORS\n` + - INPUT_PROGRAM.contents; + const RENDERED_CONTENTS = ` +// ADD IMPORTS + +// ADD EXPORTS + +// ADD CONSTANTS + +// ADD DEFINITIONS + +// REMOVE DECORATORS +` + INPUT_PROGRAM.contents; const OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, @@ -240,6 +247,22 @@ describe('Renderer', () => { expect(values[0][0].getText()) .toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`); }); + + it('should call renderImports after other abstract methods', () => { + // This allows the other methods to add additional imports if necessary + const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]); + const addExportsSpy = renderer.addExports as jasmine.Spy; + const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + const addConstantsSpy = renderer.addConstants as jasmine.Spy; + const addImportsSpy = renderer.addImports as jasmine.Spy; + renderer.renderProgram( + decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses, + moduleWithProvidersAnalyses); + expect(addExportsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addDefinitionsSpy).toHaveBeenCalledBefore(addImportsSpy); + expect(addConstantsSpy).toHaveBeenCalledBefore(addImportsSpy); + }); }); describe('source map merging', () => { @@ -355,7 +378,7 @@ describe('Renderer', () => { moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`); + expect(typingsFile.contents).toContain(`\n// ADD IMPORTS\n`); }); it('should render exports into typings files', () => { @@ -372,8 +395,7 @@ describe('Renderer', () => { moduleWithProvidersAnalyses); const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !; - expect(typingsFile.contents) - .toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`); + expect(typingsFile.contents).toContain(`\n// ADD EXPORTS\n`); }); it('should fixup functions/methods that return ModuleWithProviders structures', () => { diff --git a/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts b/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts new file mode 100644 index 0000000000..e3dd4df9a6 --- /dev/null +++ b/packages/compiler-cli/ngcc/test/rendering/umd_renderer_spec.ts @@ -0,0 +1,490 @@ +/** + * @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 MagicString from 'magic-string'; +import * as ts from 'typescript'; +import {NoopImportRewriter} from '../../../src/ngtsc/imports'; +import {AbsoluteFsPath} from '../../../src/ngtsc/path'; +import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {UmdReflectionHost} from '../../src/host/umd_host'; +import {ImportManager} from '../../../src/ngtsc/translator'; +import {UmdRenderer} from '../../src/rendering/umd_renderer'; +import {MockFileSystem} from '../helpers/mock_file_system'; +import {MockLogger} from '../helpers/mock_logger'; +import {getDeclaration, makeTestEntryPointBundle, createFileSystemFromProgramFiles} from '../helpers/utils'; + +const _ = AbsoluteFsPath.fromUnchecked; + +function setup(file: {name: string, contents: string}) { + const fs = new MockFileSystem(createFileSystemFromProgramFiles([file])); + const logger = new MockLogger(); + const bundle = makeTestEntryPointBundle('esm5', 'esm5', false, [file]); + const src = bundle.src; + const typeChecker = src.program.getTypeChecker(); + const host = new UmdReflectionHost(logger, false, src.program, src.host); + const referencesRegistry = new NgccReferencesRegistry(host); + const decorationAnalyses = new DecorationAnalyzer( + fs, src.program, src.options, src.host, typeChecker, host, + referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false) + .analyzeProgram(); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(src.program); + const renderer = new UmdRenderer(fs, logger, host, false, bundle); + const importManager = new ImportManager(new NoopImportRewriter(), 'i'); + return { + decorationAnalyses, + host, + importManager, + program: src.program, renderer, + sourceFile: src.file, switchMarkerAnalyses + }; +} + +const PROGRAM = { + name: _('/some/file.js'), + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core'], factory) : +(factory(global.file,global.someSideEffect,global.localDep,global.ng.core)); +}(this, (function (exports,someSideEffect,localDep,core) {'use strict'; +var A = (function() { + function A() {} + A.decorators = [ + { type: core.Directive, args: [{ selector: '[a]' }] }, + { type: OtherA } + ]; + A.prototype.ngDoCheck = function() { + // + }; + return A; +}()); + +var B = (function() { + function B() {} + B.decorators = [ + { type: OtherB }, + { type: core.Directive, args: [{ selector: '[b]' }] } + ]; + return B; +}()); + +var C = (function() { + function C() {} + C.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; + return C; +}()); + +function NoIife() {} + +var BadIife = (function() { + function BadIife() {} + BadIife.decorators = [ + { type: core.Directive, args: [{ selector: '[c]' }] }, + ]; +}()); + +var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; +var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable; +function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { + var compilerFactory = injector.get(CompilerFactory); + var compiler = compilerFactory.createCompiler([options]); + return compiler.compileModuleAsync(moduleType); +} + +function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { + ngDevMode && assertNgModuleType(moduleType); + return Promise.resolve(new R3NgModuleFactory(moduleType)); +} +// Some other content +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +})));`, +}; + + +const PROGRAM_DECORATE_HELPER = { + name: '/some/file.js', + contents: ` +/* A copyright notice */ +(function (global, factory) { +typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('tslib'),require('@angular/core')) : +typeof define === 'function' && define.amd ? define('file', ['exports','/tslib','@angular/core'], factory) : +(factory(global.file,global.tslib,global.ng.core)); +}(this, (function (exports,tslib,core) {'use strict'; + var OtherA = function () { return function (node) { }; }; + var OtherB = function () { return function (node) { }; }; + var A = /** @class */ (function () { + function A() { + } + A = tslib.__decorate([ + core.Directive({ selector: '[a]' }), + OtherA() + ], A); + return A; + }()); + export { A }; + var B = /** @class */ (function () { + function B() { + } + B = tslib.__decorate([ + OtherB(), + core.Directive({ selector: '[b]' }) + ], B); + return B; + }()); + export { B }; + var C = /** @class */ (function () { + function C() { + } + C = tslib.__decorate([ + core.Directive({ selector: '[c]' }) + ], C); + return C; + }()); + export { C }; + var D = /** @class */ (function () { + function D() { + } + D_1 = D; + var D_1; + D = D_1 = tslib.__decorate([ + core.Directive({ selector: '[d]', providers: [D_1] }) + ], D); + return D; + }()); + exports.D = D; + // Some other content +})));` +}; + +describe('UmdRenderer', () => { + + describe('addImports', () => { + it('should append the given imports into the CommonJS factory call', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports,require('some-side-effect'),require('/local-dep'),require('@angular/core'),require('@angular/core'),require('@angular/common')) :`); + }); + + it('should append the given imports into the AMD initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `typeof define === 'function' && define.amd ? define('file', ['exports','some-side-effect','/local-dep','@angular/core','@angular/core','@angular/common'], factory) :`); + }); + + it('should append the given imports into the global initialization', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain( + `(factory(global.file,global.someSideEffect,global.localDep,global.ng.core,global.ng.core,global.ng.common));`); + }); + + it('should append the given imports as parameters into the factory function definition', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js') !; + const output = new MagicString(PROGRAM.contents); + renderer.addImports( + output, + [ + {specifier: '@angular/core', qualifier: 'i0'}, + {specifier: '@angular/common', qualifier: 'i1'} + ], + file); + expect(output.toString()) + .toContain(`(function (exports,someSideEffect,localDep,core,i0,i1) {'use strict';`); + }); + }); + + describe('addExports', () => { + it('should insert the given exports at the end of the source file', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const generateNamedImportSpy = spyOn(importManager, 'generateNamedImport').and.callThrough(); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), identifier: 'ComponentA1'}, + {from: _('/some/a.js'), identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), identifier: 'ComponentB'}, + {from: PROGRAM.name, identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + + expect(output.toString()).toContain(` +exports.A = A; +exports.B = B; +exports.C = C; +exports.NoIife = NoIife; +exports.BadIife = BadIife; +exports.ComponentA1 = i0.ComponentA1; +exports.ComponentA2 = i0.ComponentA2; +exports.ComponentB = i1.ComponentB; +exports.TopLevelComponent = TopLevelComponent; +})));`); + + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA1'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./a', 'ComponentA2'); + expect(generateNamedImportSpy).toHaveBeenCalledWith('./foo/b', 'ComponentB'); + }); + + it('should not insert alias exports in js output', () => { + const {importManager, renderer, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + renderer.addExports( + output, PROGRAM.name.replace(/\.js$/, ''), + [ + {from: _('/some/a.js'), alias: 'eComponentA1', identifier: 'ComponentA1'}, + {from: _('/some/a.js'), alias: 'eComponentA2', identifier: 'ComponentA2'}, + {from: _('/some/foo/b.js'), alias: 'eComponentB', identifier: 'ComponentB'}, + {from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'}, + ], + importManager, sourceFile); + const outputString = output.toString(); + expect(outputString).not.toContain(`eComponentA1`); + expect(outputString).not.toContain(`eComponentB`); + expect(outputString).not.toContain(`eTopLevelComponent`); + }); + }); + + describe('addConstants', () => { + it('should insert the given constants after imports in the source file', () => { + const {renderer, program} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.addConstants(output, 'var x = 3;', file); + expect(output.toString()).toContain(` +}(this, (function (exports,someSideEffect,localDep,core) { +var x = 3; +'use strict'; +var A = (function() {`); + }); + + it('should insert constants after inserted imports', + () => { + // This test (from ESM5) is not needed as constants go in the body + // of the UMD IIFE, so cannot come before imports. + }); + }); + + describe('rewriteSwitchableDeclarations', () => { + it('should switch marked declaration initializers', () => { + const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM); + const file = program.getSourceFile('some/file.js'); + if (file === undefined) { + throw new Error(`Could not find source file`); + } + const output = new MagicString(PROGRAM.contents); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); + expect(output.toString()) + .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`); + expect(output.toString()) + .toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`); + expect(output.toString()) + .toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`); + expect(output.toString()) + .toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`); + }); + }); + + describe('addDefinitions', () => { + 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 + return A; +`); + }); + + it('should error if the compiledClass is not valid', () => { + const {renderer, sourceFile, program} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + + const noIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration); + const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'}; + expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js'); + + const badIifeDeclaration = + getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration); + const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'}; + expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT')) + .toThrowError( + 'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js'); + }); + }); + + describe('removeDecorators', () => { + + it('should delete the decorator (and following comma) that was matched in the analysis', () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()).toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .not.toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[c]' }] }`); + }); + + + it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); + const output = new MagicString(PROGRAM.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.decorators[0]; + const decoratorsToRemove = new Map(); + decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); + renderer.removeDecorators(output, decoratorsToRemove); + renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT'); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[a]' }] },`); + expect(output.toString()).toContain(`{ type: OtherA }`); + expect(output.toString()) + .toContain(`{ type: core.Directive, args: [{ selector: '[b]' }] }`); + expect(output.toString()).toContain(`{ type: OtherB }`); + 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !; + const decorator = compiledClass.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(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[c]' })`); + }); + + it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', + () => { + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !; + const decorator = compiledClass.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(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).toContain(`core.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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); + const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); + const compiledClass = + decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !; + const decorator = compiledClass.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(`core.Directive({ selector: '[a]' }),`); + expect(output.toString()).toContain(`OtherA()`); + expect(output.toString()).toContain(`core.Directive({ selector: '[b]' })`); + expect(output.toString()).toContain(`OtherB()`); + expect(output.toString()).not.toContain(`core.Directive({ selector: '[c]' })`); + expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`); + expect(output.toString()).toContain(`function C() {\n }\n return C;`); + }); + }); +});