From 632f66a461d86e4420ee8c72769cf10064dcb790 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Thu, 4 Oct 2018 12:19:11 +0100 Subject: [PATCH] refactor(ivy): ngcc - `Renderer` now manages d.ts transformation (#26082) PR Close #26082 --- .../ngcc/src/rendering/esm2015_renderer.ts | 127 ++++++--------- .../src/ngcc/src/rendering/esm5_renderer.ts | 4 +- .../ngcc/src/rendering/fesm2015_renderer.ts | 102 ++++++++++++ .../src/ngcc/src/rendering/renderer.ts | 105 ++++++++----- .../analysis/switch_marker_analyzer_spec.ts | 2 +- .../test/rendering/esm2015_renderer_spec.ts | 72 ++++----- .../ngcc/test/rendering/esm5_renderer_spec.ts | 65 ++++---- .../src/ngcc/test/rendering/renderer_spec.ts | 148 +++++++++--------- 8 files changed, 360 insertions(+), 265 deletions(-) create mode 100644 packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts 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 af9378e2c8..bc2b3991aa 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm2015_renderer.ts @@ -5,90 +5,59 @@ * 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 {relative, resolve} from 'canonical-path'; +import {readFileSync} from 'fs'; import * as ts from 'typescript'; -import MagicString from 'magic-string'; -import {POST_NGCC_MARKER, PRE_NGCC_MARKER} from '../host/ngcc_host'; -import {AnalyzedClass} from '../analysis/decoration_analyzer'; -import {Renderer} from './renderer'; -export class Esm2015Renderer extends Renderer { - /** - * Add the imports at the top of the file - */ - addImports(output: MagicString, imports: {name: string; as: string;}[]): void { - // The imports get inserted at the very top of the file. - imports.forEach(i => { output.appendLeft(0, `import * as ${i.as} from '${i.name}';\n`); }); +import {DtsFileTransformer} from '../../../ngtsc/transform'; +import {DecorationAnalysis} from '../analysis/decoration_analyzer'; +import {SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {DtsMapper} from '../host/dts_mapper'; +import {NgccReflectionHost} from '../host/ngcc_host'; + +import {Fesm2015Renderer} from './fesm2015_renderer'; +import {FileInfo} from './renderer'; + +export class Esm2015Renderer extends Fesm2015Renderer { + constructor( + protected host: NgccReflectionHost, protected isCore: boolean, + protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, + protected targetPath: string, protected dtsMapper: DtsMapper) { + super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath); } - addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { - if (constants === '') { - return; + renderFile( + sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined, + switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] { + const renderedFiles = + super.renderFile(sourceFile, decorationAnalysis, switchMarkerAnalysis, targetPath); + + // Transform the `.d.ts` files. + // TODO(gkalpak): What about `.d.ts` source maps? (See + // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.) + if (decorationAnalysis) { + // Create a `DtsFileTransformer` for the source file and record the generated fields, which + // will allow the corresponding `.d.ts` file to be transformed later. + const dtsTransformer = new DtsFileTransformer(this.rewriteCoreImportsTo, IMPORT_PREFIX); + decorationAnalysis.analyzedClasses.forEach( + analyzedClass => + dtsTransformer.recordStaticField(analyzedClass.name, analyzedClass.compilation)); + + // Find the corresponding `.d.ts` file. + const sourceFileName = sourceFile.fileName; + const originalDtsFileName = this.dtsMapper.getDtsFileNameFor(sourceFileName); + const originalDtsContents = readFileSync(originalDtsFileName, 'utf8'); + + // Transform the `.d.ts` file based on the recorded source file changes. + const transformedDtsFileName = + resolve(this.targetPath, relative(this.sourcePath, originalDtsFileName)); + const transformedDtsContents = dtsTransformer.transform(originalDtsContents, sourceFileName); + + // Add the transformed `.d.ts` file to the list of output files. + renderedFiles.push({path: transformedDtsFileName, contents: transformedDtsContents}); } - const insertionPoint = file.statements.reduce((prev, stmt) => { - if (ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) || - ts.isNamespaceImport(stmt)) { - return stmt.getEnd(); - } - return prev; - }, 0); - output.appendLeft(insertionPoint, '\n' + constants + '\n'); - } - /** - * Add the definitions to each decorated class - */ - addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void { - const classSymbol = this.host.getClassSymbol(analyzedClass.declaration); - if (!classSymbol) { - throw new Error(`Analyzed class does not have a valid symbol: ${analyzedClass.name}`); - } - const insertionPoint = classSymbol.valueDeclaration !.getEnd(); - output.appendLeft(insertionPoint, '\n' + definitions); - } - - /** - * Remove static decorator properties from classes - */ - removeDecorators(output: MagicString, decoratorsToRemove: Map): void { - decoratorsToRemove.forEach((nodesToRemove, containerNode) => { - if (ts.isArrayLiteralExpression(containerNode)) { - const items = containerNode.elements; - if (items.length === nodesToRemove.length) { - // Remove the entire statement - const statement = findStatement(containerNode); - if (statement) { - output.remove(statement.getFullStart(), statement.getEnd()); - } - } else { - nodesToRemove.forEach(node => { - // remove any trailing comma - const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? - node.getEnd() + 1 : - node.getEnd(); - output.remove(node.getFullStart(), end); - }); - } - } - }); - } - - rewriteSwitchableDeclarations(outputText: MagicString, sourceFile: ts.SourceFile): void { - const declarations = this.host.getSwitchableDeclarations(sourceFile); - declarations.forEach(declaration => { - const start = declaration.initializer.getStart(); - const end = declaration.initializer.getEnd(); - const replacement = declaration.initializer.text.replace(PRE_NGCC_MARKER, POST_NGCC_MARKER); - outputText.overwrite(start, end, replacement); - }); + return renderedFiles; } } - -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/src/rendering/esm5_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts index aadfe23a79..9581029e34 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm5_renderer.ts @@ -5,6 +5,6 @@ * 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 {Esm2015Renderer} from './esm2015_renderer'; +import {Fesm2015Renderer} from './fesm2015_renderer'; -export class Esm5Renderer extends Esm2015Renderer {} +export class Esm5Renderer extends Fesm2015Renderer {} diff --git a/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts new file mode 100644 index 0000000000..a07cfc43da --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts @@ -0,0 +1,102 @@ +/** + * @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, POST_NGCC_MARKER, PRE_NGCC_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {AnalyzedClass} from '../analysis/decoration_analyzer'; +import {Renderer} from './renderer'; + +export class Fesm2015Renderer extends Renderer { + constructor( + protected host: NgccReflectionHost, protected isCore: boolean, + protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, + protected targetPath: string) { + super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath); + } + + /** + * Add the imports at the top of the file + */ + addImports(output: MagicString, imports: {name: string; as: string;}[]): void { + // The imports get inserted at the very top of the file. + imports.forEach(i => { output.appendLeft(0, `import * as ${i.as} from '${i.name}';\n`); }); + } + + addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { + if (constants === '') { + return; + } + const insertionPoint = file.statements.reduce((prev, stmt) => { + if (ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) || + ts.isNamespaceImport(stmt)) { + return stmt.getEnd(); + } + return prev; + }, 0); + output.appendLeft(insertionPoint, '\n' + constants + '\n'); + } + + /** + * Add the definitions to each decorated class + */ + addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void { + const classSymbol = this.host.getClassSymbol(analyzedClass.declaration); + if (!classSymbol) { + throw new Error(`Analyzed class does not have a valid symbol: ${analyzedClass.name}`); + } + const insertionPoint = classSymbol.valueDeclaration !.getEnd(); + output.appendLeft(insertionPoint, '\n' + definitions); + } + + /** + * Remove static decorator properties from classes + */ + removeDecorators(output: MagicString, decoratorsToRemove: Map): void { + decoratorsToRemove.forEach((nodesToRemove, containerNode) => { + if (ts.isArrayLiteralExpression(containerNode)) { + const items = containerNode.elements; + if (items.length === nodesToRemove.length) { + // Remove the entire statement + const statement = findStatement(containerNode); + if (statement) { + output.remove(statement.getFullStart(), statement.getEnd()); + } + } else { + nodesToRemove.forEach(node => { + // remove any trailing comma + const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ? + node.getEnd() + 1 : + node.getEnd(); + output.remove(node.getFullStart(), end); + }); + } + } + }); + } + + rewriteSwitchableDeclarations( + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void { + declarations.forEach(declaration => { + const start = declaration.initializer.getStart(); + const end = declaration.initializer.getEnd(); + const replacement = declaration.initializer.text.replace(PRE_NGCC_MARKER, POST_NGCC_MARKER); + outputText.overwrite(start, end, replacement); + }); + } +} + +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/src/rendering/renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts index 1f3a55e1c2..76676eef1f 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts @@ -9,16 +9,17 @@ import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} fro import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map'; import {readFileSync, statSync} from 'fs'; import MagicString from 'magic-string'; -import {basename, dirname} from 'canonical-path'; +import {basename, dirname, relative, resolve} from 'canonical-path'; import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map'; import * as ts from 'typescript'; import {Decorator} from '../../../ngtsc/host'; import {translateStatement} from '../../../ngtsc/translator'; -import {AnalyzedClass, DecorationAnalysis} from '../analysis/decoration_analyzer'; -import {IMPORT_PREFIX} from '../constants'; -import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccImportManager} from './ngcc_import_manager'; +import {AnalyzedClass, DecorationAnalysis, DecorationAnalyses} from '../analysis/decoration_analyzer'; +import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; +import {IMPORT_PREFIX} from '../constants'; +import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host'; interface SourceMapInfo { source: string; @@ -30,10 +31,6 @@ interface SourceMapInfo { * The results of rendering an analyzed file. */ export interface RenderResult { - /** - * The file that has been rendered. - */ - file: DecorationAnalysis; /** * The rendered source file. */ @@ -67,42 +64,77 @@ export interface FileInfo { export abstract class Renderer { constructor( protected host: NgccReflectionHost, protected isCore: boolean, - protected rewriteCoreImportsTo: ts.SourceFile|null) {} + protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, + protected targetPath: string) {} + + renderProgram( + program: ts.Program, decorationAnalyses: DecorationAnalyses, + switchMarkerAnalyses: SwitchMarkerAnalyses): FileInfo[] { + const renderedFiles: FileInfo[] = []; + // Transform the source files and source maps. + program.getSourceFiles().map(sourceFile => { + const decorationAnalysis = decorationAnalyses.get(sourceFile); + const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); + + // Transform the source files and source maps. + if (decorationAnalysis || switchMarkerAnalysis) { + const targetPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName)); + renderedFiles.push( + ...this.renderFile(sourceFile, decorationAnalysis, switchMarkerAnalysis, targetPath)); + } + }); + return renderedFiles; + } /** * Render the source code and source-map for an Analyzed file. - * @param file The analyzed file to render. + * @param decorationAnalysis The analyzed file to render. * @param targetPath The absolute path where the rendered file will be written. */ - renderFile(file: DecorationAnalysis, targetPath: string): RenderResult { - const importManager = - new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX); - const input = this.extractSourceMap(file.sourceFile); - + renderFile( + sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined, + switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] { + const input = this.extractSourceMap(sourceFile); const outputText = new MagicString(input.source); - const decoratorsToRemove = new Map(); - file.analyzedClasses.forEach(clazz => { - const renderedDefinition = renderDefinitions(file.sourceFile, clazz, importManager); - this.addDefinitions(outputText, clazz, renderedDefinition); - this.trackDecorators(clazz.decorators, decoratorsToRemove); - }); + if (switchMarkerAnalysis) { + this.rewriteSwitchableDeclarations( + outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); + } - this.addConstants( - outputText, renderConstantPool(file.sourceFile, file.constantPool, importManager), - file.sourceFile); + if (decorationAnalysis) { + const importManager = + new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX); + const decoratorsToRemove = new Map(); - this.addImports( - outputText, - importManager.getAllImports(file.sourceFile.fileName, this.rewriteCoreImportsTo)); + decorationAnalysis.analyzedClasses.forEach(clazz => { + const renderedDefinition = + renderDefinitions(decorationAnalysis.sourceFile, clazz, importManager); + this.addDefinitions(outputText, clazz, renderedDefinition); + this.trackDecorators(clazz.decorators, decoratorsToRemove); + }); - // TODO: remove contructor param metadata and property decorators (we need info from the - // handlers to do this) - this.removeDecorators(outputText, decoratorsToRemove); + this.addConstants( + outputText, + renderConstantPool( + decorationAnalysis.sourceFile, decorationAnalysis.constantPool, importManager), + decorationAnalysis.sourceFile); - this.rewriteSwitchableDeclarations(outputText, file.sourceFile); + this.addImports( + outputText, importManager.getAllImports( + decorationAnalysis.sourceFile.fileName, this.rewriteCoreImportsTo)); - return this.renderSourceAndMap(file, input, outputText, targetPath); + // TODO: remove contructor param metadata and property decorators (we need info from the + // handlers to do this) + this.removeDecorators(outputText, decoratorsToRemove); + } + + const {source, map} = this.renderSourceAndMap(sourceFile, input, outputText, targetPath); + const renderedFiles = [source]; + if (map) { + renderedFiles.push(map); + } + return renderedFiles; } protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): @@ -113,7 +145,8 @@ export abstract class Renderer { protected abstract removeDecorators( output: MagicString, decoratorsToRemove: Map): void; protected abstract rewriteSwitchableDeclarations( - outputText: MagicString, sourceFile: ts.SourceFile): void; + outputText: MagicString, sourceFile: ts.SourceFile, + declarations: SwitchableVariableDeclaration[]): void; /** * Add the decorator nodes that are to be removed to a map @@ -180,11 +213,11 @@ export abstract class Renderer { * with an appropriate source-map comment pointing to the merged source-map. */ protected renderSourceAndMap( - file: DecorationAnalysis, input: SourceMapInfo, output: MagicString, + sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString, outputPath: string): RenderResult { const outputMapPath = `${outputPath}.map`; const outputMap = output.generateMap({ - source: file.sourceFile.fileName, + source: sourceFile.fileName, includeContent: true, // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix // the merge algorithm. @@ -198,13 +231,11 @@ export abstract class Renderer { if (input.isInline) { return { - file, source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}, map: null }; } else { return { - file, source: { path: outputPath, contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}` diff --git a/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts index e3e21aaccd..a69c1a54ae 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts @@ -47,7 +47,7 @@ describe('SwitchMarkerAnalyzer', () => { describe('analyzeProgram()', () => { it('should check for switchable markers in all the files of the program', () => { const program = makeProgram(...TEST_PROGRAM); - const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); const analyzer = new SwitchMarkerAnalyzer(host); const analysis = analyzer.analyzeProgram(program); 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 0894f43cf8..c40bd28ad1 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 @@ -5,24 +5,28 @@ * 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} from 'canonical-path'; import * as ts from 'typescript'; + import MagicString from 'magic-string'; import {makeProgram} from '../helpers/utils'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; +import {DtsMapper} from '../../src/host/dts_mapper'; import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Esm2015Renderer} from '../../src/rendering/esm2015_renderer'; -function setup(file: {name: string, contents: string}) { +function setup(file: {name: string, contents: string}, transformDts: boolean = false) { + const dir = dirname(file.name); + const dtsMapper = new DtsMapper(dir, dir); const program = makeProgram(file); + const sourceFile = program.getSourceFile(file.name) !; const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const analyzer = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false); - const renderer = new Esm2015Renderer(host, false, null); - return {analyzer, host, program, renderer}; -} - -function analyze(host: Fesm2015ReflectionHost, analyzer: DecorationAnalyzer, file: ts.SourceFile) { - const decoratedFiles = host.findDecoratedFiles(file); - return Array.from(decoratedFiles.values()).map(file => analyzer.analyzeFile(file))[0]; + const decorationAnalyses = + new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); + const renderer = new Esm2015Renderer(host, false, null, dir, dir, dtsMapper); + return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses}; } const PROGRAM = { @@ -133,13 +137,14 @@ export class A {}`); describe('rewriteSwitchableDeclarations', () => { it('should switch marked declaration initializers', () => { - const {renderer, program} = setup(PROGRAM); + const {renderer, program, switchMarkerAnalyses, sourceFile} = 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); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString()) .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); expect(output.toString()) @@ -157,10 +162,10 @@ export class A {}`); describe('addDefinitions', () => { it('should insert the definitions directly after the class declaration', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); + const analyzedClass = decorationAnalyses.get(sourceFile) !.analyzedClasses[0]; + renderer.addDefinitions(output, analyzedClass, 'SOME DEFINITION TEXT'); expect(output.toString()).toContain(` export class A {} SOME DEFINITION TEXT @@ -175,10 +180,10 @@ A.decorators = [ describe('[static property declaration]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -194,10 +199,10 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[1]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -213,10 +218,10 @@ A.decorators = [ it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[2]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -234,11 +239,10 @@ A.decorators = [ describe('[__decorate declarations]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'A') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); @@ -252,11 +256,10 @@ A.decorators = [ it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'B') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); @@ -271,11 +274,10 @@ A.decorators = [ it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'C') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); 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 fdf7397516..189f588828 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 @@ -9,20 +9,19 @@ import * as ts from 'typescript'; import MagicString from 'magic-string'; import {makeProgram} 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 {Esm5Renderer} from '../../src/rendering/esm5_renderer'; function setup(file: {name: string, contents: string}) { const program = makeProgram(file); + const sourceFile = program.getSourceFile(file.name) !; const host = new Esm5ReflectionHost(false, program.getTypeChecker()); - const analyzer = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false); - const renderer = new Esm5Renderer(host, false, null); - return {analyzer, host, program, renderer}; -} - -function analyze(host: Esm5ReflectionHost, analyzer: DecorationAnalyzer, file: ts.SourceFile) { - const decoratedFiles = host.findDecoratedFiles(file); - return Array.from(decoratedFiles.values()).map(file => analyzer.analyzeFile(file))[0]; + const decorationAnalyses = + new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); + const renderer = new Esm5Renderer(host, false, null, '', ''); + return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses}; } const PROGRAM = { @@ -158,13 +157,14 @@ var A = (function() {`); describe('rewriteSwitchableDeclarations', () => { it('should switch marked declaration initializers', () => { - const {renderer, program} = setup(PROGRAM); + 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); + renderer.rewriteSwitchableDeclarations( + output, file, switchMarkerAnalyses.get(sourceFile) !.declarations); expect(output.toString()) .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); expect(output.toString()) @@ -182,10 +182,10 @@ var A = (function() {`); describe('addDefinitions', () => { it('should insert the definitions directly after the class declaration', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - renderer.addDefinitions(output, analyzedFile.analyzedClasses[0], 'SOME DEFINITION TEXT'); + const analyzedClass = decorationAnalyses.get(sourceFile) !.analyzedClasses[0]; + renderer.addDefinitions(output, analyzedClass, 'SOME DEFINITION TEXT'); expect(output.toString()).toContain(` function A() {} SOME DEFINITION TEXT @@ -199,10 +199,10 @@ SOME DEFINITION TEXT describe('removeDecorators', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[0]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -217,10 +217,10 @@ SOME DEFINITION TEXT it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[1]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -236,10 +236,10 @@ SOME DEFINITION TEXT it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM); - const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const output = new MagicString(PROGRAM.contents); - const analyzedClass = analyzedFile.analyzedClasses[2]; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; const decorator = analyzedClass.decorators[0]; const decoratorsToRemove = new Map(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); @@ -257,11 +257,10 @@ SOME DEFINITION TEXT describe('[__decorate declarations]', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'A') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); @@ -275,11 +274,10 @@ SOME DEFINITION TEXT it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'B') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); @@ -294,11 +292,10 @@ SOME DEFINITION TEXT it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis', () => { - const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); - const analyzedFile = - analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !); + const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); - const analyzedClass = analyzedFile.analyzedClasses.find(c => c.name === 'C') !; + const analyzedClass = + decorationAnalyses.get(sourceFile) !.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]); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index 8304c3fb28..0a8834371a 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -11,11 +11,13 @@ import * as ts from 'typescript'; import MagicString from 'magic-string'; import {fromObject, generateMapFileComment} from 'convert-source-map'; import {makeProgram} from '../helpers/utils'; -import {AnalyzedClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; +import {AnalyzedClass, DecorationAnalyzer, DecorationAnalyses} from '../../src/analysis/decoration_analyzer'; +import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Renderer} from '../../src/rendering/renderer'; class TestRenderer extends Renderer { + constructor(host: Fesm2015ReflectionHost) { super(host, false, null, '/src', '/dist'); } addImports(output: MagicString, imports: {name: string, as: string}[]) { output.prepend('\n// ADD IMPORTS\n'); } @@ -33,118 +35,113 @@ class TestRenderer extends Renderer { } } -function createTestRenderer() { - const renderer = new TestRenderer({} as Fesm2015ReflectionHost, false, null); +function createTestRenderer(file: {name: string, contents: string}) { + const program = makeProgram(file); + const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const decorationAnalyses = + new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); + const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); + const renderer = new TestRenderer(host); spyOn(renderer, 'addImports').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough(); - return renderer as jasmine.SpyObj; -} -function analyze(file: {name: string, contents: string}) { - const program = makeProgram(file); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const analyzer = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false); - - const decoratedFiles = host.findDecoratedFiles(program.getSourceFile(file.name) !); - const analyzedFiles = Array.from(decoratedFiles.values()).map(file => analyzer.analyzeFile(file)); - - return {program, host, analyzer, decoratedFiles, analyzedFiles}; + return {renderer, program, decorationAnalyses, switchMarkerAnalyses}; } describe('Renderer', () => { const INPUT_PROGRAM = { - name: '/file.js', + name: '/src/file.js', contents: `import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n` }; + const INPUT_PROGRAM_MAP = fromObject({ 'version': 3, - 'file': '/file.js', + 'file': '/src/file.js', 'sourceRoot': '', - 'sources': ['/file.ts'], + 'sources': ['/src/file.ts'], 'names': [], 'mappings': 'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC', - 'sourcesContent': [ - 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x: string): string {\n return x;\n }\n static decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n ];\n}' - ] + 'sourcesContent': [INPUT_PROGRAM.contents] }); + const RENDERED_CONTENTS = - `\n// REWRITTEN DECLARATIONS\n\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` + + `\n// REMOVE DECORATORS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n` + INPUT_PROGRAM.contents; + const OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, - 'file': '/output_file.js', - 'sources': ['/file.js'], - 'sourcesContent': [ - 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n];\n' - ], + 'file': '/dist/file.js', + 'sources': ['/src/file.js'], + 'sourcesContent': [INPUT_PROGRAM.contents], 'names': [], - 'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' + 'mappings': ';;;;;;;;AAAA;;;;;;;;;' }); const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ 'version': 3, - 'sources': ['/file.ts'], + 'sources': ['/src/file.ts'], 'names': [], - 'mappings': ';;;;;;;;;;AAAA', - 'file': '/output_file.js', - 'sourcesContent': [ - 'import { Directive } from \'@angular/core\';\nexport class A {\n foo(x: string): string {\n return x;\n }\n static decorators = [\n { type: Directive, args: [{ selector: \'[a]\' }] }\n ];\n}' - ] + 'mappings': ';;;;;;;;AAAA', + 'file': '/dist/file.js', + 'sourcesContent': [INPUT_PROGRAM.contents] }); - describe('renderFile()', () => { + describe('renderProgram()', () => { it('should render the modified contents; and a new map file, if the original provided no map file.', () => { - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze(INPUT_PROGRAM); - const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(result.source.path).toEqual('/output_file.js'); - expect(result.source.contents) - .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/output_file.js.map')); - expect(result.map !.path).toEqual('/output_file.js.map'); - expect(result.map !.contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); + const {renderer, program, decorationAnalyses, switchMarkerAnalyses} = + createTestRenderer(INPUT_PROGRAM); + const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + expect(result[0].path).toEqual('/dist/file.js'); + expect(result[0].contents) + .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map')); + expect(result[1].path).toEqual('/dist/file.js.map'); + expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); }); it('should call addImports with the source code and info about the core Angular library.', () => { - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze(INPUT_PROGRAM); - renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(renderer.addImports.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); - expect(renderer.addImports.calls.first().args[1]).toEqual([ + const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = + createTestRenderer(INPUT_PROGRAM); + renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + const addImportsSpy = renderer.addImports as jasmine.Spy; + expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); + expect(addImportsSpy.calls.first().args[1]).toEqual([ {name: '@angular/core', as: 'ɵngcc0'} ]); }); it('should call addDefinitions with the source code, the analyzed class and the renderered definitions.', () => { - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze(INPUT_PROGRAM); - renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(renderer.addDefinitions.calls.first().args[0].toString()) - .toEqual(RENDERED_CONTENTS); - expect(renderer.addDefinitions.calls.first().args[1]) - .toBe(analyzedFiles[0].analyzedClasses[0]); - expect(renderer.addDefinitions.calls.first().args[2]) + const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = + createTestRenderer(INPUT_PROGRAM); + renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy; + expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); + expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({ + name: 'A', + decorators: [jasmine.objectContaining({name: 'Directive'})], + })); + expect(addDefinitionsSpy.calls.first().args[2]) .toEqual( `A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); }, features: [ɵngcc0.ɵPublicFeature] });`); }); it('should call removeDecorators with the source code, a map of class decorators that have been analyzed', () => { - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze(INPUT_PROGRAM); - renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(renderer.removeDecorators.calls.first().args[0].toString()) - .toEqual(RENDERED_CONTENTS); + const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = + createTestRenderer(INPUT_PROGRAM); + renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy; + expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); // Each map key is the TS node of the decorator container // Each map value is an array of TS nodes that are the decorators to remove - const map = renderer.removeDecorators.calls.first().args[1] as Map; + const map = removeDecoratorsSpy.calls.first().args[1] as Map; const keys = Array.from(map.keys()); expect(keys.length).toEqual(1); expect(keys[0].getText()) @@ -157,34 +154,31 @@ describe('Renderer', () => { it('should merge any inline source map from the original file and write the output as an inline source map', () => { - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze({ + const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() }); - const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(result.source.path).toEqual('/output_file.js'); - expect(result.source.contents) + const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + expect(result[0].path).toEqual('/dist/file.js'); + expect(result[0].contents) .toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); - expect(result.map).toBe(null); + expect(result[1]).toBeUndefined(); }); it('should merge any external source map from the original file and write the output to an external source map', () => { // Mock out reading the map file from disk - const readFileSyncSpy = - spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); - const renderer = createTestRenderer(); - const {analyzedFiles} = analyze({ + spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); + const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({ ...INPUT_PROGRAM, contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' }); - const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); - expect(result.source.path).toEqual('/output_file.js'); - expect(result.source.contents) - .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/output_file.js.map')); - expect(result.map !.path).toEqual('/output_file.js.map'); - expect(result.map !.contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); + const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses); + expect(result[0].path).toEqual('/dist/file.js'); + expect(result[0].contents) + .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map')); + expect(result[1].path).toEqual('/dist/file.js.map'); + expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); }); }); });