refactor(ivy): ngcc - `Renderer` now manages d.ts transformation (#26082)

PR Close #26082
This commit is contained in:
Pete Bacon Darwin 2018-10-04 12:19:11 +01:00 committed by Miško Hevery
parent f7b17a4784
commit 632f66a461
8 changed files with 360 additions and 265 deletions

View File

@ -5,90 +5,59 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 * 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 { import {DtsFileTransformer} from '../../../ngtsc/transform';
/** import {DecorationAnalysis} from '../analysis/decoration_analyzer';
* Add the imports at the top of the file import {SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
*/ import {IMPORT_PREFIX} from '../constants';
addImports(output: MagicString, imports: {name: string; as: string;}[]): void { import {DtsMapper} from '../host/dts_mapper';
// The imports get inserted at the very top of the file. import {NgccReflectionHost} from '../host/ngcc_host';
imports.forEach(i => { output.appendLeft(0, `import * as ${i.as} from '${i.name}';\n`); });
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 { renderFile(
if (constants === '') { sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined,
return; 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');
}
/** return renderedFiles;
* 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<ts.Node, ts.Node[]>): 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);
});
} }
} }
function findStatement(node: ts.Node) {
while (node) {
if (ts.isExpressionStatement(node)) {
return node;
}
node = node.parent;
}
return undefined;
}

View File

@ -5,6 +5,6 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * 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 {}

View File

@ -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<ts.Node, ts.Node[]>): 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;
}

View File

@ -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 {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
import {readFileSync, statSync} from 'fs'; import {readFileSync, statSync} from 'fs';
import MagicString from 'magic-string'; 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 {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/host'; import {Decorator} from '../../../ngtsc/host';
import {translateStatement} from '../../../ngtsc/translator'; 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 {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 { interface SourceMapInfo {
source: string; source: string;
@ -30,10 +31,6 @@ interface SourceMapInfo {
* The results of rendering an analyzed file. * The results of rendering an analyzed file.
*/ */
export interface RenderResult { export interface RenderResult {
/**
* The file that has been rendered.
*/
file: DecorationAnalysis;
/** /**
* The rendered source file. * The rendered source file.
*/ */
@ -67,42 +64,77 @@ export interface FileInfo {
export abstract class Renderer { export abstract class Renderer {
constructor( constructor(
protected host: NgccReflectionHost, protected isCore: boolean, 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. * 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. * @param targetPath The absolute path where the rendered file will be written.
*/ */
renderFile(file: DecorationAnalysis, targetPath: string): RenderResult { renderFile(
const importManager = sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined,
new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX); switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] {
const input = this.extractSourceMap(file.sourceFile); const input = this.extractSourceMap(sourceFile);
const outputText = new MagicString(input.source); const outputText = new MagicString(input.source);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
file.analyzedClasses.forEach(clazz => { if (switchMarkerAnalysis) {
const renderedDefinition = renderDefinitions(file.sourceFile, clazz, importManager); this.rewriteSwitchableDeclarations(
this.addDefinitions(outputText, clazz, renderedDefinition); outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
this.trackDecorators(clazz.decorators, decoratorsToRemove); }
});
this.addConstants( if (decorationAnalysis) {
outputText, renderConstantPool(file.sourceFile, file.constantPool, importManager), const importManager =
file.sourceFile); new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
this.addImports( decorationAnalysis.analyzedClasses.forEach(clazz => {
outputText, const renderedDefinition =
importManager.getAllImports(file.sourceFile.fileName, this.rewriteCoreImportsTo)); 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 this.addConstants(
// handlers to do this) outputText,
this.removeDecorators(outputText, decoratorsToRemove); 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): protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
@ -113,7 +145,8 @@ export abstract class Renderer {
protected abstract removeDecorators( protected abstract removeDecorators(
output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void; output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void;
protected abstract rewriteSwitchableDeclarations( 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 * 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. * with an appropriate source-map comment pointing to the merged source-map.
*/ */
protected renderSourceAndMap( protected renderSourceAndMap(
file: DecorationAnalysis, input: SourceMapInfo, output: MagicString, sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString,
outputPath: string): RenderResult { outputPath: string): RenderResult {
const outputMapPath = `${outputPath}.map`; const outputMapPath = `${outputPath}.map`;
const outputMap = output.generateMap({ const outputMap = output.generateMap({
source: file.sourceFile.fileName, source: sourceFile.fileName,
includeContent: true, includeContent: true,
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix // hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
// the merge algorithm. // the merge algorithm.
@ -198,13 +231,11 @@ export abstract class Renderer {
if (input.isInline) { if (input.isInline) {
return { return {
file,
source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`}, source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`},
map: null map: null
}; };
} else { } else {
return { return {
file,
source: { source: {
path: outputPath, path: outputPath,
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}` contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`

View File

@ -47,7 +47,7 @@ describe('SwitchMarkerAnalyzer', () => {
describe('analyzeProgram()', () => { describe('analyzeProgram()', () => {
it('should check for switchable markers in all the files of the program', () => { it('should check for switchable markers in all the files of the program', () => {
const program = makeProgram(...TEST_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 analyzer = new SwitchMarkerAnalyzer(host);
const analysis = analyzer.analyzeProgram(program); const analysis = analyzer.analyzeProgram(program);

View File

@ -5,24 +5,28 @@
* Use of this source code is governed by an MIT-style license that can be * 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 * found in the LICENSE file at https://angular.io/license
*/ */
import {dirname} from 'canonical-path';
import * as ts from 'typescript'; import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {makeProgram} from '../helpers/utils'; import {makeProgram} from '../helpers/utils';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {DtsMapper} from '../../src/host/dts_mapper';
import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host';
import {Esm2015Renderer} from '../../src/rendering/esm2015_renderer'; 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 program = makeProgram(file);
const sourceFile = program.getSourceFile(file.name) !;
const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); const host = new Fesm2015ReflectionHost(false, program.getTypeChecker());
const analyzer = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false); const decorationAnalyses =
const renderer = new Esm2015Renderer(host, false, null); new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program);
return {analyzer, host, program, renderer}; const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
} const renderer = new Esm2015Renderer(host, false, null, dir, dir, dtsMapper);
return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses};
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 PROGRAM = { const PROGRAM = {
@ -133,13 +137,14 @@ export class A {}`);
describe('rewriteSwitchableDeclarations', () => { describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => { 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'); const file = program.getSourceFile('some/file.js');
if (file === undefined) { if (file === undefined) {
throw new Error(`Could not find source file`); throw new Error(`Could not find source file`);
} }
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(output, file); renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
expect(output.toString()) expect(output.toString())
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); .not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`);
expect(output.toString()) expect(output.toString())
@ -157,10 +162,10 @@ export class A {}`);
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const {analyzer, host, program, renderer} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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(` expect(output.toString()).toContain(`
export class A {} export class A {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
@ -175,10 +180,10 @@ A.decorators = [
describe('[static property declaration]', () => { describe('[static property declaration]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', it('should delete the decorator (and following comma) that was matched in the analysis',
() => { () => {
const {analyzer, host, program, renderer} = setup(PROGRAM); const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
@ -234,11 +239,10 @@ A.decorators = [
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);

View File

@ -9,20 +9,19 @@ import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {makeProgram} from '../helpers/utils'; import {makeProgram} from '../helpers/utils';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {Esm5Renderer} from '../../src/rendering/esm5_renderer'; import {Esm5Renderer} from '../../src/rendering/esm5_renderer';
function setup(file: {name: string, contents: string}) { function setup(file: {name: string, contents: string}) {
const program = makeProgram(file); const program = makeProgram(file);
const sourceFile = program.getSourceFile(file.name) !;
const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const analyzer = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false); const decorationAnalyses =
const renderer = new Esm5Renderer(host, false, null); new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program);
return {analyzer, host, program, renderer}; const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
} const renderer = new Esm5Renderer(host, false, null, '', '');
return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses};
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 PROGRAM = { const PROGRAM = {
@ -158,13 +157,14 @@ var A = (function() {`);
describe('rewriteSwitchableDeclarations', () => { describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => { 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'); const file = program.getSourceFile('some/file.js');
if (file === undefined) { if (file === undefined) {
throw new Error(`Could not find source file`); throw new Error(`Could not find source file`);
} }
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(output, file); renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
expect(output.toString()) expect(output.toString())
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`); .not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_NGCC__;`);
expect(output.toString()) expect(output.toString())
@ -182,10 +182,10 @@ var A = (function() {`);
describe('addDefinitions', () => { describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const {analyzer, host, program, renderer} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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(` expect(output.toString()).toContain(`
function A() {} function A() {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
@ -199,10 +199,10 @@ SOME DEFINITION TEXT
describe('removeDecorators', () => { describe('removeDecorators', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {analyzer, host, program, renderer} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const analyzedFile = analyze(host, analyzer, program.getSourceFile(PROGRAM.name) !);
const output = new MagicString(PROGRAM.contents); 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 decorator = analyzedClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
@ -257,11 +257,10 @@ SOME DEFINITION TEXT
describe('[__decorate declarations]', () => { describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => { it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {analyzer, host, program, renderer} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); 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', 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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const analyzedFile =
analyze(host, analyzer, program.getSourceFile(PROGRAM_DECORATE_HELPER.name) !);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); 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 decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);

View File

@ -11,11 +11,13 @@ import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {fromObject, generateMapFileComment} from 'convert-source-map'; import {fromObject, generateMapFileComment} from 'convert-source-map';
import {makeProgram} from '../helpers/utils'; 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 {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host';
import {Renderer} from '../../src/rendering/renderer'; import {Renderer} from '../../src/rendering/renderer';
class TestRenderer extends Renderer { class TestRenderer extends Renderer {
constructor(host: Fesm2015ReflectionHost) { super(host, false, null, '/src', '/dist'); }
addImports(output: MagicString, imports: {name: string, as: string}[]) { addImports(output: MagicString, imports: {name: string, as: string}[]) {
output.prepend('\n// ADD IMPORTS\n'); output.prepend('\n// ADD IMPORTS\n');
} }
@ -33,118 +35,113 @@ class TestRenderer extends Renderer {
} }
} }
function createTestRenderer() { function createTestRenderer(file: {name: string, contents: string}) {
const renderer = new TestRenderer({} as Fesm2015ReflectionHost, false, null); 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, 'addImports').and.callThrough();
spyOn(renderer, 'addDefinitions').and.callThrough(); spyOn(renderer, 'addDefinitions').and.callThrough();
spyOn(renderer, 'removeDecorators').and.callThrough(); spyOn(renderer, 'removeDecorators').and.callThrough();
return renderer as jasmine.SpyObj<TestRenderer>;
}
function analyze(file: {name: string, contents: string}) { return {renderer, program, decorationAnalyses, switchMarkerAnalyses};
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};
} }
describe('Renderer', () => { describe('Renderer', () => {
const INPUT_PROGRAM = { const INPUT_PROGRAM = {
name: '/file.js', name: '/src/file.js',
contents: 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` `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({ const INPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'file': '/file.js', 'file': '/src/file.js',
'sourceRoot': '', 'sourceRoot': '',
'sources': ['/file.ts'], 'sources': ['/src/file.ts'],
'names': [], 'names': [],
'mappings': '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', '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': [ 'sourcesContent': [INPUT_PROGRAM.contents]
'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}'
]
}); });
const RENDERED_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; INPUT_PROGRAM.contents;
const OUTPUT_PROGRAM_MAP = fromObject({ const OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'file': '/output_file.js', 'file': '/dist/file.js',
'sources': ['/file.js'], 'sources': ['/src/file.js'],
'sourcesContent': [ 'sourcesContent': [INPUT_PROGRAM.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'
],
'names': [], 'names': [],
'mappings': ';;;;;;;;;;AAAA;;;;;;;;;' 'mappings': ';;;;;;;;AAAA;;;;;;;;;'
}); });
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({ const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3, 'version': 3,
'sources': ['/file.ts'], 'sources': ['/src/file.ts'],
'names': [], 'names': [],
'mappings': ';;;;;;;;;;AAAA', 'mappings': ';;;;;;;;AAAA',
'file': '/output_file.js', 'file': '/dist/file.js',
'sourcesContent': [ 'sourcesContent': [INPUT_PROGRAM.contents]
'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}'
]
}); });
describe('renderFile()', () => { describe('renderProgram()', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.', it('should render the modified contents; and a new map file, if the original provided no map file.',
() => { () => {
const renderer = createTestRenderer(); const {renderer, program, decorationAnalyses, switchMarkerAnalyses} =
const {analyzedFiles} = analyze(INPUT_PROGRAM); createTestRenderer(INPUT_PROGRAM);
const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result.source.path).toEqual('/output_file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result.source.contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/output_file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
expect(result.map !.path).toEqual('/output_file.js.map'); expect(result[1].path).toEqual('/dist/file.js.map');
expect(result.map !.contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON()); expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON());
}); });
it('should call addImports with the source code and info about the core Angular library.', it('should call addImports with the source code and info about the core Angular library.',
() => { () => {
const renderer = createTestRenderer(); const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
const {analyzedFiles} = analyze(INPUT_PROGRAM); createTestRenderer(INPUT_PROGRAM);
renderer.renderFile(analyzedFiles[0], '/output_file.js'); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(renderer.addImports.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(renderer.addImports.calls.first().args[1]).toEqual([ expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([
{name: '@angular/core', as: 'ɵngcc0'} {name: '@angular/core', as: 'ɵngcc0'}
]); ]);
}); });
it('should call addDefinitions with the source code, the analyzed class and the renderered definitions.', it('should call addDefinitions with the source code, the analyzed class and the renderered definitions.',
() => { () => {
const renderer = createTestRenderer(); const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
const {analyzedFiles} = analyze(INPUT_PROGRAM); createTestRenderer(INPUT_PROGRAM);
renderer.renderFile(analyzedFiles[0], '/output_file.js'); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(renderer.addDefinitions.calls.first().args[0].toString()) const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
.toEqual(RENDERED_CONTENTS); expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(renderer.addDefinitions.calls.first().args[1]) expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
.toBe(analyzedFiles[0].analyzedClasses[0]); name: 'A',
expect(renderer.addDefinitions.calls.first().args[2]) decorators: [jasmine.objectContaining({name: 'Directive'})],
}));
expect(addDefinitionsSpy.calls.first().args[2])
.toEqual( .toEqual(
`A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); }, features: [ɵngcc0.ɵPublicFeature] });`); `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', it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => { () => {
const renderer = createTestRenderer(); const {decorationAnalyses, program, renderer, switchMarkerAnalyses} =
const {analyzedFiles} = analyze(INPUT_PROGRAM); createTestRenderer(INPUT_PROGRAM);
renderer.renderFile(analyzedFiles[0], '/output_file.js'); renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(renderer.removeDecorators.calls.first().args[0].toString()) const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
.toEqual(RENDERED_CONTENTS); expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
// Each map key is the TS node of the decorator container // 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 // 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<ts.Node, ts.Node[]>; const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>;
const keys = Array.from(map.keys()); const keys = Array.from(map.keys());
expect(keys.length).toEqual(1); expect(keys.length).toEqual(1);
expect(keys[0].getText()) 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', it('should merge any inline source map from the original file and write the output as an inline source map',
() => { () => {
const renderer = createTestRenderer(); const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({
const {analyzedFiles} = analyze({
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment() contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}); });
const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result.source.path).toEqual('/output_file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result.source.contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment()); .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', 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 // Mock out reading the map file from disk
const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON()); const {decorationAnalyses, program, renderer, switchMarkerAnalyses} = createTestRenderer({
const renderer = createTestRenderer();
const {analyzedFiles} = analyze({
...INPUT_PROGRAM, ...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map' contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}); });
const result = renderer.renderFile(analyzedFiles[0], '/output_file.js'); const result = renderer.renderProgram(program, decorationAnalyses, switchMarkerAnalyses);
expect(result.source.path).toEqual('/output_file.js'); expect(result[0].path).toEqual('/dist/file.js');
expect(result.source.contents) expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/output_file.js.map')); .toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
expect(result.map !.path).toEqual('/output_file.js.map'); expect(result[1].path).toEqual('/dist/file.js.map');
expect(result.map !.contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON()); expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON());
}); });
}); });
}); });