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
* 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;
}
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');
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});
}
/**
* 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);
});
return renderedFiles;
}
}
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
* 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 {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 {
renderFile(
sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] {
const input = this.extractSourceMap(sourceFile);
const outputText = new MagicString(input.source);
if (switchMarkerAnalysis) {
this.rewriteSwitchableDeclarations(
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
}
if (decorationAnalysis) {
const importManager =
new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX);
const input = this.extractSourceMap(file.sourceFile);
const outputText = new MagicString(input.source);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
file.analyzedClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(file.sourceFile, clazz, importManager);
decorationAnalysis.analyzedClasses.forEach(clazz => {
const renderedDefinition =
renderDefinitions(decorationAnalysis.sourceFile, clazz, importManager);
this.addDefinitions(outputText, clazz, renderedDefinition);
this.trackDecorators(clazz.decorators, decoratorsToRemove);
});
this.addConstants(
outputText, renderConstantPool(file.sourceFile, file.constantPool, importManager),
file.sourceFile);
outputText,
renderConstantPool(
decorationAnalysis.sourceFile, decorationAnalysis.constantPool, importManager),
decorationAnalysis.sourceFile);
this.addImports(
outputText,
importManager.getAllImports(file.sourceFile.fileName, this.rewriteCoreImportsTo));
outputText, importManager.getAllImports(
decorationAnalysis.sourceFile.fileName, this.rewriteCoreImportsTo));
// TODO: remove contructor param metadata and property decorators (we need info from the
// handlers to do this)
this.removeDecorators(outputText, decoratorsToRemove);
}
this.rewriteSwitchableDeclarations(outputText, file.sourceFile);
return this.renderSourceAndMap(file, input, outputText, targetPath);
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<ts.Node, ts.Node[]>): 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)}`

View File

@ -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);

View File

@ -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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.Node[]>();
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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 {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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.Node[]>();
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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',
() => {
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<ts.Node, ts.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 {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<TestRenderer>;
}
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<ts.Node, ts.Node[]>;
const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>;
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({
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());
});
});
});