fix(ivy): ngcc - fixes to support compiling Material library (#26403)

1) The `DecorationAnalyzer now analyzes all source files, rather than just
the entry-point files, which fixes #26183.
2) The `DecoratorAnalyzer` now runs all the `handler.analyze()`  calls
across the whole entry-point *before* running `handler.compile()`. This
ensures that dependencies between the decorated classes *within* an
entry-point are known to the handlers when running the compile process.
3) The `Renderer` now does the transformation of the typings (.d.ts) files
which allows us to support packages that only have flat format
entry-points better, and is faster, since we won't parse `.d.ts` files twice.

PR Close #26403
This commit is contained in:
Pete Bacon Darwin 2018-10-16 08:56:54 +01:00 committed by Kara Erickson
parent dff10085e8
commit 030d43b9f3
17 changed files with 536 additions and 417 deletions

View File

@ -13,24 +13,29 @@ import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHa
import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform'; import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class'; import {DecoratedClass} from '../host/decorated_class';
import {DecoratedFile} from '../host/decorated_file';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils'; import {isDefined} from '../utils';
export interface AnalyzedClass<A = any, M = any> extends DecoratedClass { export interface AnalyzedFile {
handler: DecoratorHandler<A, M>; sourceFile: ts.SourceFile;
analysis: any; analyzedClasses: AnalyzedClass[];
diagnostics?: ts.Diagnostic[];
compilation: CompileResult[];
} }
export interface DecorationAnalysis { export interface AnalyzedClass extends DecoratedClass {
analyzedClasses: AnalyzedClass[]; diagnostics?: ts.Diagnostic[];
handler: DecoratorHandler<any, any>;
analysis: any;
}
export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; }
export interface CompiledFile {
compiledClasses: CompiledClass[];
sourceFile: ts.SourceFile; sourceFile: ts.SourceFile;
constantPool: ConstantPool; constantPool: ConstantPool;
} }
export type DecorationAnalyses = Map<ts.SourceFile, DecorationAnalysis>; export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>;
export const DecorationAnalyses = Map; export const DecorationAnalyses = Map;
export interface MatchingHandler<A, M> { export interface MatchingHandler<A, M> {
@ -72,58 +77,59 @@ export class DecorationAnalyzer {
* @returns a map of the source files to the analysis for those files. * @returns a map of the source files to the analysis for those files.
*/ */
analyzeProgram(program: ts.Program): DecorationAnalyses { analyzeProgram(program: ts.Program): DecorationAnalyses {
const analyzedFiles = new DecorationAnalyses(); const decorationAnalyses = new DecorationAnalyses();
program.getRootFileNames().forEach(fileName => { const analysedFiles =
const entryPoint = program.getSourceFile(fileName) !; program.getSourceFiles().map(sourceFile => this.analyzeFile(sourceFile)).filter(isDefined);
const decoratedFiles = this.host.findDecoratedFiles(entryPoint); const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
decoratedFiles.forEach( compiledFiles.forEach(
decoratedFile => compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
analyzedFiles.set(decoratedFile.sourceFile, this.analyzeFile(decoratedFile))); return decorationAnalyses;
});
return analyzedFiles;
} }
/** protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
* Analyze a decorated file to generate the information about decorated classes that const decoratedClasses = this.host.findDecoratedClasses(sourceFile);
* should be converted to use ivy definitions. return decoratedClasses.length ? {
* @param file The file to be analysed for decorated classes. sourceFile,
* @returns the analysis of the file analyzedClasses: decoratedClasses.map(clazz => this.analyzeClass(clazz)).filter(isDefined)
*/ } :
protected analyzeFile(file: DecoratedFile): DecorationAnalysis { undefined;
const constantPool = new ConstantPool();
const analyzedClasses =
file.decoratedClasses.map(clazz => this.analyzeClass(constantPool, clazz))
.filter(isDefined);
return {
analyzedClasses,
sourceFile: file.sourceFile, constantPool,
};
} }
protected analyzeClass(pool: ConstantPool, clazz: DecoratedClass): AnalyzedClass|undefined { protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
const matchingHandlers = this.handlers const matchingHandlers = this.handlers
.map(handler => ({ .map(handler => {
handler, const match =
match: handler.detect(clazz.declaration, clazz.decorators), handler.detect(clazz.declaration, clazz.decorators);
})) return {handler, match};
})
.filter(isMatchingHandler); .filter(isMatchingHandler);
if (matchingHandlers.length > 1) { if (matchingHandlers.length > 1) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
} }
if (matchingHandlers.length === 0) { if (matchingHandlers.length === 0) {
return undefined; return null;
} }
const {handler, match} = matchingHandlers[0]; const {handler, match} = matchingHandlers[0];
const {analysis, diagnostics} = handler.analyze(clazz.declaration, match); const {analysis, diagnostics} = handler.analyze(clazz.declaration, match);
let compilation = handler.compile(clazz.declaration, analysis, pool); return {...clazz, handler, analysis, diagnostics};
}
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
const constantPool = new ConstantPool();
const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => {
const compilation = this.compileClass(analyzedClass, constantPool);
return {...analyzedClass, compilation};
});
return {constantPool, sourceFile: analyzedFile.sourceFile, compiledClasses};
}
protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] {
let compilation = clazz.handler.compile(clazz.declaration, clazz.analysis, constantPool);
if (!Array.isArray(compilation)) { if (!Array.isArray(compilation)) {
compilation = [compilation]; compilation = [compilation];
} }
return {...clazz, handler, analysis, diagnostics, compilation}; return compilation;
} }
} }

View File

@ -1,21 +0,0 @@
/**
* @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 {DecoratedClass} from './decorated_class';
/**
* Information about a source file that contains decorated exported classes.
*/
export class DecoratedFile {
/**
* The decorated exported classes that have been found in the file.
*/
public decoratedClasses: DecoratedClass[] = [];
constructor(public sourceFile: ts.SourceFile) {}
}

View File

@ -1,32 +0,0 @@
/**
* @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 {relative, resolve} from 'canonical-path';
/**
* Map source files to their associated typings definitions files.
*/
export class DtsMapper {
constructor(private sourceRoot: string, private dtsRoot: string) {}
/**
* Given the absolute path to a source file, return the absolute path to the corresponding `.d.ts`
* file. Assume that source files and `.d.ts` files have the same directory layout and the names
* of the `.d.ts` files can be derived by replacing the `.js` extension of the source file with
* `.d.ts`.
*
* @param sourceFileName The absolute path to the source file whose corresponding `.d.ts` file
* should be returned.
*
* @returns The absolute path to the `.d.ts` file that corresponds to the specified source file.
*/
getDtsFileNameFor(sourceFileName: string): string {
const relativeSourcePath = relative(this.sourceRoot, sourceFileName);
return resolve(this.dtsRoot, relativeSourcePath).replace(/\.js$/, '.d.ts');
}
}

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {readFileSync} from 'fs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host'; import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host';
@ -14,8 +13,6 @@ import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/met
import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils'; import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils';
import {DecoratedClass} from './decorated_class'; import {DecoratedClass} from './decorated_class';
import {DecoratedFile} from './decorated_file';
import {DtsMapper} from './dts_mapper';
import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String; export const DECORATORS = 'decorators' as ts.__String;
@ -51,8 +48,14 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
* a static method called `ctorParameters`. * a static method called `ctorParameters`.
*/ */
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
constructor(protected isCore: boolean, checker: ts.TypeChecker, protected dtsMapper?: DtsMapper) { protected dtsClassMap: Map<string, ts.ClassDeclaration>|null;
constructor(
protected isCore: boolean, checker: ts.TypeChecker, dtsRootFileName?: string,
dtsProgram?: ts.Program|null) {
super(checker); super(checker);
this.dtsClassMap = (dtsRootFileName && dtsProgram) ?
this.computeDtsClassMap(dtsRootFileName, dtsProgram) :
null;
} }
/** /**
@ -73,12 +76,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (!symbol) { if (!symbol) {
return null; return null;
} }
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); return this.getDecoratorsOfSymbol(symbol);
if (decoratorsProperty) {
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
} else {
return this.getClassDecoratorsFromHelperCall(symbol);
}
} }
/** /**
@ -206,10 +204,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (ts.isClassDeclaration(declaration)) { if (ts.isClassDeclaration(declaration)) {
return declaration.name && this.checker.getSymbolAtLocation(declaration.name); return declaration.name && this.checker.getSymbolAtLocation(declaration.name);
} }
if (ts.isVariableDeclaration(declaration) && declaration.initializer && if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
ts.isClassExpression(declaration.initializer)) { declaration = declaration.initializer;
return declaration.initializer.name && }
this.checker.getSymbolAtLocation(declaration.initializer.name); if (ts.isClassExpression(declaration)) {
return declaration.name && this.checker.getSymbolAtLocation(declaration.name);
} }
return undefined; return undefined;
} }
@ -299,46 +298,29 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id);
} }
/* /**
* Find all the files accessible via an entry-point, that contain decorated classes. * Find all the classes that contain decorations in a given file.
* @param entryPoint The starting point file for finding files that contain decorated classes. * @param sourceFile The source file to search for decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import * @returns An array of decorated classes.
* information.
*/ */
findDecoratedFiles(entryPoint: ts.SourceFile): Map<ts.SourceFile, DecoratedFile> { findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] {
const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); const classes: DecoratedClass[] = [];
const map = new Map<ts.SourceFile, DecoratedFile>(); sourceFile.statements.map(statement => {
if (moduleSymbol) { if (ts.isVariableStatement(statement)) {
const exportedSymbols = statement.declarationList.declarations.forEach(declaration => {
this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker)); const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration));
const exportedDeclarations = if (decoratedClass) {
exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined); classes.push(decoratedClass);
}
const decoratedClasses = });
exportedDeclarations } else if (ts.isClassDeclaration(statement)) {
.map(declaration => { const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(statement));
if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { if (decoratedClass) {
const name = declaration.name && ts.isIdentifier(declaration.name) ? classes.push(decoratedClass);
declaration.name.text :
undefined;
const decorators = this.getDecoratorsOfDeclaration(declaration);
return decorators && isDefined(name) ?
new DecoratedClass(name, declaration, decorators) :
undefined;
}
return undefined;
})
.filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new DecoratedFile(file));
} }
map.get(file) !.decoratedClasses.push(clazz); }
}); });
} return classes;
return map;
} }
/** /**
@ -348,21 +330,38 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* is not a class or has an unknown number of type parameters. * is not a class or has an unknown number of type parameters.
*/ */
getGenericArityOfClass(clazz: ts.Declaration): number|null { getGenericArityOfClass(clazz: ts.Declaration): number|null {
if (this.dtsMapper && ts.isClassDeclaration(clazz) && clazz.name) { const dtsClass = this.getDtsDeclarationOfClass(clazz);
const sourcePath = clazz.getSourceFile(); if (dtsClass) {
const dtsPath = this.dtsMapper.getDtsFileNameFor(sourcePath.fileName); return dtsClass.typeParameters ? dtsClass.typeParameters.length : 0;
const dtsContents = readFileSync(dtsPath, 'utf8'); }
// TODO: investigate caching parsed .d.ts files as they're needed for several different return null;
// purposes in ngcc. }
const dtsFile = ts.createSourceFile(
dtsPath, dtsContents, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
for (let i = dtsFile.statements.length - 1; i >= 0; i--) { /**
const stmt = dtsFile.statements[i]; * Take an exported declaration of a class (maybe downleveled to a variable) and look up the
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined && * declaration of its type in a separate .d.ts tree.
stmt.name.text === clazz.name.text) { *
return stmt.typeParameters ? stmt.typeParameters.length : 0; * This function is allowed to return `null` if the current compilation unit does not have a
* separate .d.ts tree. When compiling TypeScript code this is always the case, since .d.ts files
* are produced only during the emit of such a compilation. When compiling .js code, however,
* there is frequently a parallel .d.ts tree which this method exposes.
*
* Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclarationOfClass(declaration: ts.Declaration): ts.ClassDeclaration|null {
if (this.dtsClassMap) {
if (ts.isClassDeclaration(declaration)) {
if (!declaration.name || !ts.isIdentifier(declaration.name)) {
throw new Error(
`Cannot get the dts file for a class declaration that has no indetifier: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
} }
const dtsDeclaration = this.dtsClassMap.get(declaration.name.text);
if (!dtsDeclaration) {
throw new Error(
`Unable to find matching typings (.d.ts) declaration for ${declaration.name.text} in ${declaration.getSourceFile().fileName}`);
}
return dtsDeclaration;
} }
} }
return null; return null;
@ -371,6 +370,25 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
protected getDecoratorsOfSymbol(symbol: ts.Symbol): Decorator[]|null {
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
if (decoratorsProperty) {
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
} else {
return this.getClassDecoratorsFromHelperCall(symbol);
}
}
protected getDecoratedClassFromSymbol(symbol: ts.Symbol|undefined): DecoratedClass|null {
if (symbol) {
const decorators = this.getDecoratorsOfSymbol(symbol);
if (decorators && decorators.length) {
return new DecoratedClass(symbol.name, symbol.valueDeclaration, decorators);
}
}
return null;
}
/** /**
* Walk the AST looking for an assignment to the specified symbol. * Walk the AST looking for an assignment to the specified symbol.
* @param node The current node we are searching. * @param node The current node we are searching.
@ -691,7 +709,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
let value: ts.Expression|null = null; let value: ts.Expression|null = null;
let name: string|null = null; let name: string|null = null;
let nameNode: ts.Identifier|null = null; let nameNode: ts.Identifier|null = null;
let type = null;
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
@ -744,6 +761,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
} }
const type: ts.TypeNode = (node as any).type || null;
return { return {
node, node,
implementation: node, kind, type, name, nameNode, value, isStatic, implementation: node, kind, type, name, nameNode, value, isStatic,
@ -967,13 +985,41 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* *
* @param decorator the decorator to test. * @param decorator the decorator to test.
*/ */
isFromCore(decorator: Decorator): boolean { protected isFromCore(decorator: Decorator): boolean {
if (this.isCore) { if (this.isCore) {
return !decorator.import || /^\./.test(decorator.import.from); return !decorator.import || /^\./.test(decorator.import.from);
} else { } else {
return !!decorator.import && decorator.import.from === '@angular/core'; return !!decorator.import && decorator.import.from === '@angular/core';
} }
} }
protected computeDtsClassMap(dtsRootFileName: string, dtsProgram: ts.Program):
Map<string, ts.ClassDeclaration> {
const dtsClassMap = new Map<string, ts.ClassDeclaration>();
const checker = dtsProgram.getTypeChecker();
const dtsRootFile = dtsProgram.getSourceFile(dtsRootFileName);
const rootModule = dtsRootFile && checker.getSymbolAtLocation(dtsRootFile);
const moduleExports = rootModule && checker.getExportsOfModule(rootModule);
if (moduleExports) {
moduleExports.forEach(exportedSymbol => {
if (exportedSymbol.flags & ts.SymbolFlags.Alias) {
exportedSymbol = checker.getAliasedSymbol(exportedSymbol);
}
const declaration = exportedSymbol.declarations[0];
if (declaration && ts.isClassDeclaration(declaration)) {
const name = exportedSymbol.name;
const previousDeclaration = dtsClassMap.get(name);
if (previousDeclaration && previousDeclaration !== declaration) {
console.warn(
`Ambiguous class name ${name} in typings files: ${previousDeclaration.getSourceFile().fileName} and ${declaration.getSourceFile().fileName}`);
} else {
dtsClassMap.set(name, declaration);
}
}
});
}
return dtsClassMap;
}
} }
///////////// Exported Helpers ///////////// ///////////// Exported Helpers /////////////

View File

@ -10,10 +10,8 @@ import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host'; import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host';
import {reflectObjectLiteral} from '../../../ngtsc/metadata'; import {reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText, getOriginalSymbol, isDefined} from '../utils'; import {getNameText} from '../utils';
import {DecoratedClass} from './decorated_class';
import {DecoratedFile} from './decorated_file';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
@ -125,41 +123,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
return {node, body: statements || null, parameters}; return {node, body: statements || null, parameters};
} }
/**
* Find all the files accessible via an entry-point, that contain decorated classes.
* @param entryPoint The starting point file for finding files that contain decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import
* information.
*/
findDecoratedFiles(entryPoint: ts.SourceFile): Map<ts.SourceFile, DecoratedFile> {
const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint);
const map = new Map<ts.SourceFile, DecoratedFile>();
const getParsedClass = (declaration: ts.VariableDeclaration) => {
const decorators = this.getDecoratorsOfDeclaration(declaration);
if (decorators) {
return new DecoratedClass(getNameText(declaration.name), declaration, decorators);
}
};
if (moduleSymbol) {
const classDeclarations = this.checker.getExportsOfModule(moduleSymbol)
.map(getOriginalSymbol(this.checker))
.map(exportSymbol => exportSymbol.valueDeclaration)
.filter(isDefined)
.filter(ts.isVariableDeclaration);
const decoratedClasses = classDeclarations.map(getParsedClass).filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new DecoratedFile(file));
}
map.get(file) !.decoratedClasses.push(clazz);
});
}
return map;
}
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////

View File

@ -7,7 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReflectionHost} from '../../../ngtsc/host'; import {ReflectionHost} from '../../../ngtsc/host';
import {DecoratedFile} from './decorated_file'; import {DecoratedClass} from './decorated_class';
export const PRE_R3_MARKER = '__PRE_R3__'; export const PRE_R3_MARKER = '__PRE_R3__';
export const POST_R3_MARKER = '__POST_R3__'; export const POST_R3_MARKER = '__POST_R3__';
@ -40,10 +40,9 @@ export interface NgccReflectionHost extends ReflectionHost {
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[]; getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
/** /**
* Find all the files accessible via an entry-point, that contain decorated classes. * Find all the classes that contain decorations in a given file.
* @param entryPoint The starting point file for finding files that contain decorated classes. * @param sourceFile The source file to search for decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import * @returns An array of decorated classes.
* information.
*/ */
findDecoratedFiles(entryPoint: ts.SourceFile): Map<ts.SourceFile, DecoratedFile>; findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[];
} }

View File

@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as path from 'canonical-path'; import * as path from 'canonical-path';
import {existsSync, lstatSync, readFileSync, readdirSync} from 'fs';
import * as yargs from 'yargs'; import * as yargs from 'yargs';
import {DependencyHost} from './packages/dependency_host'; import {DependencyHost} from './packages/dependency_host';
@ -48,8 +47,14 @@ export function mainNgcc(args: string[]): number {
try { try {
const {entryPoints} = finder.findEntryPoints(sourcePath); const {entryPoints} = finder.findEntryPoints(sourcePath);
entryPoints.forEach( entryPoints.forEach(entryPoint => {
entryPoint => formats.forEach(format => transformer.transform(entryPoint, format))); // We transform the d.ts typings files while transforming one of the formats.
// This variable decides with which of the available formats to do this transform.
// It is marginally faster to process via the flat file if available.
const dtsTranformFormat: EntryPointFormat = entryPoint.fesm2015 ? 'fesm2015' : 'esm2015';
formats.forEach(
format => transformer.transform(entryPoint, format, format === dtsTranformFormat));
});
} catch (e) { } catch (e) {
console.error(e.stack); console.error(e.stack);
return 1; return 1;

View File

@ -12,7 +12,6 @@ import * as ts from 'typescript';
import {DecorationAnalyzer} from '../analysis/decoration_analyzer'; import {DecorationAnalyzer} from '../analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
import {DtsMapper} from '../host/dts_mapper';
import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host'; import {Esm5ReflectionHost} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host'; import {NgccReflectionHost} from '../host/ngcc_host';
@ -44,7 +43,7 @@ import {EntryPoint, EntryPointFormat} from './entry_point';
export class Transformer { export class Transformer {
constructor(private sourcePath: string, private targetPath: string) {} constructor(private sourcePath: string, private targetPath: string) {}
transform(entryPoint: EntryPoint, format: EntryPointFormat): void { transform(entryPoint: EntryPoint, format: EntryPointFormat, transformDts: boolean): void {
if (checkMarkerFile(entryPoint, format)) { if (checkMarkerFile(entryPoint, format)) {
console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`); console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`);
return; return;
@ -73,19 +72,24 @@ export class Transformer {
const r3SymbolsPath = isCore ? this.findR3SymbolsPath(dirname(entryPointFilePath)) : null; const r3SymbolsPath = isCore ? this.findR3SymbolsPath(dirname(entryPointFilePath)) : null;
const rootPaths = r3SymbolsPath ? [entryPointFilePath, r3SymbolsPath] : [entryPointFilePath]; const rootPaths = r3SymbolsPath ? [entryPointFilePath, r3SymbolsPath] : [entryPointFilePath];
const packageProgram = ts.createProgram(rootPaths, options, host); const packageProgram = ts.createProgram(rootPaths, options, host);
const dtsMapper = new DtsMapper(dirname(entryPointFilePath), dirname(entryPoint.typings)); console.time(entryPoint.name + '(dtsmappper creation)');
const reflectionHost = this.getHost(isCore, format, packageProgram, dtsMapper); const dtsFilePath = entryPoint.typings;
const dtsProgram = transformDts ? ts.createProgram([entryPoint.typings], options, host) : null;
console.timeEnd(entryPoint.name + '(dtsmappper creation)');
const reflectionHost = this.getHost(isCore, format, packageProgram, dtsFilePath, dtsProgram);
const r3SymbolsFile = r3SymbolsPath && packageProgram.getSourceFile(r3SymbolsPath) || null; const r3SymbolsFile = r3SymbolsPath && packageProgram.getSourceFile(r3SymbolsPath) || null;
// Parse and analyze the files. // Parse and analyze the files.
const {decorationAnalyses, switchMarkerAnalyses} = const {decorationAnalyses, switchMarkerAnalyses} =
this.analyzeProgram(packageProgram, reflectionHost, rootDirs, isCore); this.analyzeProgram(packageProgram, reflectionHost, rootDirs, isCore);
console.time(entryPoint.name + '(rendering)');
// Transform the source files and source maps. // Transform the source files and source maps.
const renderer = const renderer =
this.getRenderer(format, packageProgram, reflectionHost, isCore, r3SymbolsFile, dtsMapper); this.getRenderer(format, packageProgram, reflectionHost, isCore, r3SymbolsFile);
const renderedFiles = const renderedFiles =
renderer.renderProgram(packageProgram, decorationAnalyses, switchMarkerAnalyses); renderer.renderProgram(packageProgram, decorationAnalyses, switchMarkerAnalyses);
console.timeEnd(entryPoint.name + '(rendering)');
// Write out all the transformed files. // Write out all the transformed files.
renderedFiles.forEach(file => this.writeFile(file)); renderedFiles.forEach(file => this.writeFile(file));
@ -104,12 +108,13 @@ export class Transformer {
} }
} }
getHost(isCore: boolean, format: string, program: ts.Program, dtsMapper: DtsMapper): getHost(
NgccReflectionHost { isCore: boolean, format: string, program: ts.Program, dtsFilePath: string,
dtsProgram: ts.Program|null): NgccReflectionHost {
switch (format) { switch (format) {
case 'esm2015': case 'esm2015':
case 'fesm2015': case 'fesm2015':
return new Esm2015ReflectionHost(isCore, program.getTypeChecker(), dtsMapper); return new Esm2015ReflectionHost(isCore, program.getTypeChecker(), dtsFilePath, dtsProgram);
case 'esm5': case 'esm5':
case 'fesm5': case 'fesm5':
return new Esm5ReflectionHost(isCore, program.getTypeChecker()); return new Esm5ReflectionHost(isCore, program.getTypeChecker());
@ -120,13 +125,14 @@ export class Transformer {
getRenderer( getRenderer(
format: string, program: ts.Program, host: NgccReflectionHost, isCore: boolean, format: string, program: ts.Program, host: NgccReflectionHost, isCore: boolean,
rewriteCoreImportsTo: ts.SourceFile|null, dtsMapper: DtsMapper|null): Renderer { rewriteCoreImportsTo: ts.SourceFile|null): Renderer {
switch (format) { switch (format) {
case 'esm2015': case 'esm2015':
case 'esm5': case 'esm5':
case 'fesm2015': case 'fesm2015':
case 'fesm5': case 'fesm5':
return new EsmRenderer(host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath, dtsMapper); return new EsmRenderer(
host, isCore, rewriteCoreImportsTo, this.sourcePath, this.targetPath);
default: default:
throw new Error(`Renderer for "${format}" not yet implemented.`); throw new Error(`Renderer for "${format}" not yet implemented.`);
} }

View File

@ -7,17 +7,16 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import MagicString from 'magic-string'; import MagicString from 'magic-string';
import {DtsMapper} from '../host/dts_mapper';
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {AnalyzedClass} from '../analysis/decoration_analyzer'; import {CompiledClass} from '../analysis/decoration_analyzer';
import {Renderer} from './renderer'; import {Renderer} from './renderer';
export class EsmRenderer extends Renderer { export class EsmRenderer extends Renderer {
constructor( constructor(
protected host: NgccReflectionHost, protected isCore: boolean, protected host: NgccReflectionHost, protected isCore: boolean,
protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string,
protected targetPath: string, dtsMapper: DtsMapper|null) { protected targetPath: string) {
super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath, dtsMapper); super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath);
} }
/** /**
@ -45,10 +44,10 @@ export class EsmRenderer extends Renderer {
/** /**
* Add the definitions to each decorated class * Add the definitions to each decorated class
*/ */
addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void { addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(analyzedClass.declaration); const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) { if (!classSymbol) {
throw new Error(`Analyzed class does not have a valid symbol: ${analyzedClass.name}`); throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
} }
const insertionPoint = classSymbol.valueDeclaration !.getEnd(); const insertionPoint = classSymbol.valueDeclaration !.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions); output.appendLeft(insertionPoint, '\n' + definitions);

View File

@ -14,13 +14,12 @@ 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 {DtsFileTransformer} from '../../../ngtsc/transform'; import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
import {translateStatement} from '../../../ngtsc/translator'; import {translateStatement, translateType} from '../../../ngtsc/translator';
import {NgccImportManager} from './ngcc_import_manager'; import {NgccImportManager} from './ngcc_import_manager';
import {AnalyzedClass, DecorationAnalysis, DecorationAnalyses} from '../analysis/decoration_analyzer'; import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../constants'; import {IMPORT_PREFIX} from '../constants';
import {DtsMapper} from '../host/dts_mapper';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host'; import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
interface SourceMapInfo { interface SourceMapInfo {
@ -29,20 +28,6 @@ interface SourceMapInfo {
isInline: boolean; isInline: boolean;
} }
/**
* The results of rendering an analyzed file.
*/
export interface RenderResult {
/**
* The rendered source file.
*/
source: FileInfo;
/**
* The rendered source map file.
*/
map: FileInfo|null;
}
/** /**
* Information about a file that has been rendered. * Information about a file that has been rendered.
*/ */
@ -57,6 +42,11 @@ export interface FileInfo {
contents: string; contents: string;
} }
interface DtsClassInfo {
dtsDeclaration: ts.Declaration;
compilation: CompileResult[];
}
/** /**
* A base-class for rendering an `AnalyzedFile`. * A base-class for rendering an `AnalyzedFile`.
* *
@ -67,39 +57,38 @@ export abstract class Renderer {
constructor( constructor(
protected host: NgccReflectionHost, protected isCore: boolean, protected host: NgccReflectionHost, protected isCore: boolean,
protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string, protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string,
protected targetPath: string, protected dtsMapper: DtsMapper|null) {} protected targetPath: string) {}
renderProgram( renderProgram(
program: ts.Program, decorationAnalyses: DecorationAnalyses, program: ts.Program, decorationAnalyses: DecorationAnalyses,
switchMarkerAnalyses: SwitchMarkerAnalyses): FileInfo[] { switchMarkerAnalyses: SwitchMarkerAnalyses): FileInfo[] {
const renderedFiles: FileInfo[] = []; const renderedFiles: FileInfo[] = [];
// Transform the source files, source maps and typings files. // Transform the source files.
program.getSourceFiles().map(sourceFile => { program.getSourceFiles().map(sourceFile => {
const decorationAnalysis = decorationAnalyses.get(sourceFile); const compiledFile = decorationAnalyses.get(sourceFile);
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile); const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
if (decorationAnalysis || switchMarkerAnalysis) { if (compiledFile || switchMarkerAnalysis) {
const targetPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName)); renderedFiles.push(...this.renderFile(sourceFile, compiledFile, switchMarkerAnalysis));
renderedFiles.push(
...this.renderFile(sourceFile, decorationAnalysis, switchMarkerAnalysis, targetPath));
}
if (decorationAnalyses) {
renderedFiles.push(...this.renderTypings(decorationAnalyses));
} }
}); });
// Transform the .d.ts files
const dtsFiles = this.getTypingsFilesToRender(decorationAnalyses);
dtsFiles.forEach((classes, file) => renderedFiles.push(...this.renderDtsFile(file, classes)));
return renderedFiles; 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 decorationAnalysis The analyzed file to render. * @param compiledFile 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( renderFile(
sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined, sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] { switchMarkerAnalysis: SwitchMarkerAnalysis|undefined): FileInfo[] {
const input = this.extractSourceMap(sourceFile); const input = this.extractSourceMap(sourceFile);
const outputText = new MagicString(input.source); const outputText = new MagicString(input.source);
@ -108,46 +97,59 @@ export abstract class Renderer {
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations); outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
} }
if (decorationAnalysis) { if (compiledFile) {
const importManager = const importManager =
new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX); new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>(); const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decorationAnalysis.analyzedClasses.forEach(clazz => { compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
renderDefinitions(decorationAnalysis.sourceFile, clazz, importManager);
this.addDefinitions(outputText, clazz, renderedDefinition); this.addDefinitions(outputText, clazz, renderedDefinition);
this.trackDecorators(clazz.decorators, decoratorsToRemove); this.trackDecorators(clazz.decorators, decoratorsToRemove);
}); });
this.addConstants( this.addConstants(
outputText, outputText,
renderConstantPool( renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
decorationAnalysis.sourceFile, decorationAnalysis.constantPool, importManager), compiledFile.sourceFile);
decorationAnalysis.sourceFile);
this.addImports( this.addImports(
outputText, importManager.getAllImports( outputText,
decorationAnalysis.sourceFile.fileName, this.rewriteCoreImportsTo)); importManager.getAllImports(compiledFile.sourceFile.fileName, this.rewriteCoreImportsTo));
// TODO: remove contructor param metadata and property decorators (we need info from the // TODO: remove contructor param metadata and property decorators (we need info from the
// handlers to do this) // handlers to do this)
this.removeDecorators(outputText, decoratorsToRemove); this.removeDecorators(outputText, decoratorsToRemove);
} }
const {source, map} = this.renderSourceAndMap(sourceFile, input, outputText, targetPath); return this.renderSourceAndMap(sourceFile, input, outputText);
const renderedFiles = [source]; }
if (map) {
renderedFiles.push(map); renderDtsFile(dtsFile: ts.SourceFile, dtsClasses: DtsClassInfo[]): FileInfo[] {
} const input = this.extractSourceMap(dtsFile);
return renderedFiles; const outputText = new MagicString(input.source);
const importManager = new NgccImportManager(false, this.isCore, IMPORT_PREFIX);
dtsClasses.forEach(dtsClass => {
const endOfClass = dtsClass.dtsDeclaration.getEnd();
dtsClass.compilation.forEach(declaration => {
const type = translateType(declaration.type, importManager);
const newStatement = ` static ${declaration.name}: ${type};\n`;
outputText.appendRight(endOfClass - 1, newStatement);
});
});
this.addImports(
outputText, importManager.getAllImports(dtsFile.fileName, this.rewriteCoreImportsTo));
return this.renderSourceAndMap(dtsFile, input, outputText);
} }
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void; void;
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void; protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void;
protected abstract addDefinitions( protected abstract addDefinitions(
output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void; output: MagicString, compiledClass: CompiledClass, definitions: string): void;
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(
@ -219,8 +221,8 @@ 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(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString, sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
outputPath: string): RenderResult { const outputPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
const outputMapPath = `${outputPath}.map`; const outputMapPath = `${outputPath}.map`;
const outputMap = output.generateMap({ const outputMap = output.generateMap({
source: sourceFile.fileName, source: sourceFile.fileName,
@ -235,41 +237,34 @@ export abstract class Renderer {
const mergedMap = const mergedMap =
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString())); mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
const result: FileInfo[] = [];
if (input.isInline) { if (input.isInline) {
return { result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`},
map: null
};
} else { } else {
return { result.push({
source: { path: outputPath,
path: outputPath, contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}` });
}, result.push({path: outputMapPath, contents: mergedMap.toJSON()});
map: {path: outputMapPath, contents: mergedMap.toJSON()}
};
} }
return result;
} }
// TODO(gkalpak): What about `.d.ts` source maps? (See protected getTypingsFilesToRender(analyses: DecorationAnalyses):
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.) Map<ts.SourceFile, DtsClassInfo[]> {
renderTypings(decorationAnalyses: DecorationAnalyses): FileInfo[] { const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>();
const renderedFiles: FileInfo[] = []; analyses.forEach(compiledFile => {
if (this.dtsMapper) { compiledFile.compiledClasses.forEach(compiledClass => {
const dtsTransformer = new DtsFileTransformer(this.rewriteCoreImportsTo, IMPORT_PREFIX); const dtsDeclaration = this.host.getDtsDeclarationOfClass(compiledClass.declaration);
decorationAnalyses.forEach((analysis, sourceFile) => { if (dtsDeclaration) {
const sourceFileName = sourceFile.fileName; const dtsFile = dtsDeclaration.getSourceFile();
const dtsFileName = this.dtsMapper !.getDtsFileNameFor(sourceFileName); const classes = dtsMap.get(dtsFile) || [];
const dtsContents = readFileSync(dtsFileName, 'utf8'); classes.push({dtsDeclaration, compilation: compiledClass.compilation});
analysis.analyzedClasses.forEach(analyzedClass => dtsTransformer.recordStaticField(analyzedClass.name, analyzedClass.compilation)); dtsMap.set(dtsFile, classes);
const newDtsFileName = resolve(this.targetPath, relative(this.sourcePath, dtsFileName)); }
const newDtsContents = dtsTransformer.transform(dtsContents, sourceFileName);
renderedFiles.push({path: newDtsFileName, contents: newDtsContents});
}); });
} });
return renderedFiles; return dtsMap;
// }
} }
} }
@ -321,11 +316,11 @@ export function renderConstantPool(
* @param imports An object that tracks the imports that are needed by the rendered definitions. * @param imports An object that tracks the imports that are needed by the rendered definitions.
*/ */
export function renderDefinitions( export function renderDefinitions(
sourceFile: ts.SourceFile, analyzedClass: AnalyzedClass, imports: NgccImportManager): string { sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: NgccImportManager): string {
const printer = ts.createPrinter(); const printer = ts.createPrinter();
const name = (analyzedClass.declaration as ts.NamedDeclaration).name !; const name = (compiledClass.declaration as ts.NamedDeclaration).name !;
const definitions = const definitions =
analyzedClass.compilation compiledClass.compilation
.map( .map(
c => c.statements.map(statement => translateStatement(statement, imports)) c => c.statements.map(statement => translateStatement(statement, imports))
.concat(translateStatement( .concat(translateStatement(

View File

@ -27,6 +27,34 @@ const TEST_PROGRAM = {
` `
}; };
const INTERNAL_COMPONENT_PROGRAM = [
{
name: 'entrypoint.js',
contents: `
import {Component, NgModule} from '@angular/core';
import {ImportedComponent} from './component';
export class LocalComponent {}
LocalComponent.decorators = [{type: Component}];
export class MyModule {}
MyModule.decorators = [{type: NgModule, args: [{
declarations: [ImportedComponent, LocalComponent],
exports: [ImportedComponent, LocalComponent],
},] }];
`
},
{
name: 'component.js',
contents: `
import {Component} from '@angular/core';
export class ImportedComponent {}
ImportedComponent.decorators = [{type: Component}];
`,
isRoot: false,
}
];
function createTestHandler() { function createTestHandler() {
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [ const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
'detect', 'detect',
@ -79,9 +107,9 @@ describe('DecorationAnalyzer', () => {
it('should return an object containing the classes that were analyzed', () => { it('should return an object containing the classes that were analyzed', () => {
const file = program.getSourceFile(TEST_PROGRAM.name) !; const file = program.getSourceFile(TEST_PROGRAM.name) !;
const analysis = result.get(file) !; const compiledFile = result.get(file) !;
expect(analysis.analyzedClasses.length).toEqual(1); expect(compiledFile.compiledClasses.length).toEqual(1);
expect(analysis.analyzedClasses[0].name).toEqual('MyComponent'); expect(compiledFile.compiledClasses[0].name).toEqual('MyComponent');
}); });
it('should analyze and compile the classes that are detected', () => { it('should analyze and compile the classes that are detected', () => {
@ -91,5 +119,41 @@ describe('DecorationAnalyzer', () => {
expect(testHandler.compile).toHaveBeenCalledTimes(1); expect(testHandler.compile).toHaveBeenCalledTimes(1);
expect(testHandler.compile.calls.allArgs()[0][1]).toEqual('Component'); expect(testHandler.compile.calls.allArgs()[0][1]).toEqual('Component');
}); });
describe('internal components', () => {
// The problem of exposing the type of these internal components in the .d.ts typing files
// is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const program = makeProgram(...INTERNAL_COMPONENT_PROGRAM);
const analyzer = new DecorationAnalyzer(
program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()),
[''], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram(program);
const file = program.getSourceFile('component.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const ImportedComponent =
analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !;
expect(ImportedComponent).toBeDefined();
});
it('should analyze an internally defined component, which is not exported at all', () => {
const program = makeProgram(...INTERNAL_COMPONENT_PROGRAM);
const analyzer = new DecorationAnalyzer(
program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()),
[''], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram(program);
const file = program.getSourceFile('entrypoint.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
expect(LocalComponent).toBeDefined();
});
});
}); });
}); });

View File

@ -6,10 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as fs from 'fs';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMemberKind, Import} from '../../../ngtsc/host'; import {ClassMemberKind, Import} from '../../../ngtsc/host';
import {DtsMapper} from '../../src/host/dts_mapper';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getDeclaration, makeProgram} from '../helpers/utils'; import {getDeclaration, makeProgram} from '../helpers/utils';
@ -399,6 +397,7 @@ const DECORATED_FILES = [
name: '/primary.js', name: '/primary.js',
contents: ` contents: `
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
import {D} from '/secondary';
class A {} class A {}
A.decorators = [ A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] } { type: Directive, args: [{ selector: '[a]' }] }
@ -411,7 +410,6 @@ const DECORATED_FILES = [
]; ];
class C {} class C {}
export { A, x, C }; export { A, x, C };
export { D } from '/secondary';
` `
}, },
{ {
@ -422,7 +420,7 @@ const DECORATED_FILES = [
D.decorators = [ D.decorators = [
{ type: Directive, args: [{ selector: '[d]' }] } { type: Directive, args: [{ selector: '[d]' }] }
]; ];
export { D }; export {D};
` `
} }
]; ];
@ -439,13 +437,38 @@ const ARITY_CLASSES = [
{ {
name: '/typings/class.d.ts', name: '/typings/class.d.ts',
contents: ` contents: `
export class NoTypeParam {} export declare class NoTypeParam {}
export class OneTypeParam<T> {} export declare class OneTypeParam<T> {}
export class TwoTypeParams<T, K> {} export declare class TwoTypeParams<T, K> {}
`, `,
}, },
]; ];
const TYPINGS_SRC_FILES = [
{name: '/src/index.js', contents: `export * from './class1'; export * from './class2';`},
{name: '/src/class1.js', contents: 'export class Class1 {}\nexport class MissingClass1 {}'},
{name: '/src/class2.js', contents: 'export class Class2 {}'},
{name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, {
name: '/src/flat-file.js',
contents:
'export class Class1 {}\nexport class MissingClass1 {}\nexport class MissingClass2 {}\class Class3 {}\nexport {Class3 as xClass3};',
}
];
const TYPINGS_DTS_FILES = [
{name: '/typings/index.d.ts', contents: `export * from './class1'; export * from './class2';`},
{
name: '/typings/class1.d.ts',
contents: `export declare class Class1 {}\nexport declare class OtherClass {}`
},
{
name: '/typings/class2.d.ts',
contents:
`export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';`
},
{name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`},
];
describe('Fesm2015ReflectionHost', () => { describe('Fesm2015ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
@ -1186,12 +1209,10 @@ describe('Fesm2015ReflectionHost', () => {
describe('getGenericArityOfClass()', () => { describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => { it('should properly count type parameters', () => {
// Mock out reading the `d.ts` file from disk const dtsProgram = makeProgram(ARITY_CLASSES[1]);
const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(ARITY_CLASSES[1].contents);
const program = makeProgram(ARITY_CLASSES[0]); const program = makeProgram(ARITY_CLASSES[0]);
const host = new Esm2015ReflectionHost(
const dtsMapper = new DtsMapper('/src', '/typings'); false, program.getTypeChecker(), ARITY_CLASSES[1].name, dtsProgram);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsMapper);
const noTypeParamClass = const noTypeParamClass =
getDeclaration(program, '/src/class.js', 'NoTypeParam', ts.isClassDeclaration); getDeclaration(program, '/src/class.js', 'NoTypeParam', ts.isClassDeclaration);
expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0); expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0);
@ -1217,30 +1238,95 @@ describe('Fesm2015ReflectionHost', () => {
}); });
}); });
describe('findDecoratedFiles()', () => { describe('findDecoratedClasses()', () => {
it('should return an array of objects for each file that has exported and decorated classes', it('should return an array of all decorated classes in the given source file', () => {
() => { const program = makeProgram(...DECORATED_FILES);
const program = makeProgram(...DECORATED_FILES); const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !;
const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !; const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !;
const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !;
const decoratedFiles = host.findDecoratedFiles(primaryFile);
expect(decoratedFiles.size).toEqual(2); const primaryDecoratedClasses = host.findDecoratedClasses(primaryFile);
expect(primaryDecoratedClasses.length).toEqual(2);
const classA = primaryDecoratedClasses.find(c => c.name === 'A') !;
expect(ts.isClassDeclaration(classA.declaration)).toBeTruthy();
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
// Note that `B` is not exported from `primary.js`
const classB = primaryDecoratedClasses.find(c => c.name === 'B') !;
expect(ts.isClassDeclaration(classB.declaration)).toBeTruthy();
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const primary = decoratedFiles.get(primaryFile) !; const secondaryDecoratedClasses = host.findDecoratedClasses(secondaryFile) !;
expect(primary.decoratedClasses.length).toEqual(1); expect(secondaryDecoratedClasses.length).toEqual(1);
const classA = primary.decoratedClasses.find(c => c.name === 'A') !; // Note that `D` is exported from `secondary.js` but not exported from `primary.js`
expect(classA.name).toEqual('A'); const classD = secondaryDecoratedClasses.find(c => c.name === 'D') !;
expect(ts.isClassDeclaration(classA.declaration)).toBeTruthy(); expect(classD.name).toEqual('D');
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); expect(ts.isClassDeclaration(classD.declaration)).toBeTruthy();
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = decoratedFiles.get(secondaryFile) !; });
expect(secondary.decoratedClasses.length).toEqual(1);
const classD = secondary.decoratedClasses.find(c => c.name === 'D') !;
expect(classD.name).toEqual('D');
expect(ts.isClassDeclaration(classD.declaration)).toBeTruthy();
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
}); });
describe('getDtsDeclarationsOfClass()', () => {
it('should find the dts declaration that has the same relative path to the source file', () => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const class1 = getDeclaration(srcProgram, '/src/class1.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
const dtsDeclaration = host.getDtsDeclarationOfClass(class1);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
});
it('should throw an error if there is no matching class in the matching dts file', () => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const missingClass =
getDeclaration(srcProgram, '/src/class1.js', 'MissingClass1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
expect(() => host.getDtsDeclarationOfClass(missingClass))
.toThrowError(
'Unable to find matching typings (.d.ts) declaration for MissingClass1 in /src/class1.js');
});
it('should throw an error if there is no matching dts file', () => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const missingClass = getDeclaration(
srcProgram, '/src/missing-class.js', 'MissingClass2', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
expect(() => host.getDtsDeclarationOfClass(missingClass))
.toThrowError(
'Unable to find matching typings (.d.ts) declaration for MissingClass2 in /src/missing-class.js');
});
it('should find the dts file that contains a matching class declaration, even if the source files do not match',
() => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const class1 =
getDeclaration(srcProgram, '/src/flat-file.js', 'Class1', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
const dtsDeclaration = host.getDtsDeclarationOfClass(class1);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class1.d.ts');
});
it('should find aliased exports', () => {
const srcProgram = makeProgram(...TYPINGS_SRC_FILES);
const dtsProgram = makeProgram(...TYPINGS_DTS_FILES);
const class3 =
getDeclaration(srcProgram, '/src/flat-file.js', 'Class3', ts.isClassDeclaration);
const host = new Esm2015ReflectionHost(
false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram);
const dtsDeclaration = host.getDtsDeclarationOfClass(class3);
expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts');
});
});
}); });

View File

@ -432,6 +432,7 @@ const DECORATED_FILES = [
name: '/primary.js', name: '/primary.js',
contents: ` contents: `
import {Directive} from '@angular/core'; import {Directive} from '@angular/core';
import { D } from '/secondary';
var A = (function() { var A = (function() {
function A() {} function A() {}
A.decorators = [ A.decorators = [
@ -453,7 +454,6 @@ const DECORATED_FILES = [
return C; return C;
}); });
export { A, x, C }; export { A, x, C };
export { D } from '/secondary';
` `
}, },
{ {
@ -1252,26 +1252,27 @@ describe('Esm5ReflectionHost', () => {
}); });
}); });
describe('fileDecoratedFiles()', () => { describe('findDecoratedClasses()', () => {
it('should return an array of objects for each file that has exported and decorated classes', it('should return an array of all decorated classes in the given source file', () => {
() => { const program = makeProgram(...DECORATED_FILES);
const program = makeProgram(...DECORATED_FILES); const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const host = new Esm5ReflectionHost(false, program.getTypeChecker()); const primary = program.getSourceFile(DECORATED_FILES[0].name) !;
const primary = program.getSourceFile(DECORATED_FILES[0].name) !;
const decoratedFiles = host.findDecoratedFiles(primary);
expect(decoratedFiles.size).toEqual(2);
const primaryClasses = decoratedFiles.get(primary) !.decoratedClasses;
expect(primaryClasses.length).toEqual(1);
const classA = primaryClasses.find(c => c.name === 'A') !;
expect(classA.name).toEqual('A');
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = program.getSourceFile(DECORATED_FILES[1].name) !; const primaryDecoratedClasses = host.findDecoratedClasses(primary);
const secondaryClasses = decoratedFiles.get(secondary) !.decoratedClasses; expect(primaryDecoratedClasses.length).toEqual(2);
expect(secondaryClasses.length).toEqual(1); const classA = primaryDecoratedClasses.find(c => c.name === 'A') !;
const classD = secondaryClasses.find(c => c.name === 'D') !; expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
expect(classD.name).toEqual('D'); // Note that `B` is not exported from `primary.js`
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); const classB = primaryDecoratedClasses.find(c => c.name === 'B') !;
}); expect(classB.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = program.getSourceFile(DECORATED_FILES[1].name) !;
const secondaryDecoratedClasses = host.findDecoratedClasses(secondary);
expect(secondaryDecoratedClasses.length).toEqual(1);
// Note that `D` is exported from `secondary.js` but not exported from `primary.js`
const classD = secondaryDecoratedClasses.find(c => c.name === 'D') !;
expect(classD.name).toEqual('D');
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
}); });
}); });

View File

@ -12,7 +12,6 @@ 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 {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {DtsMapper} from '../../src/host/dts_mapper';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {EsmRenderer} from '../../src/rendering/esm_renderer'; import {EsmRenderer} from '../../src/rendering/esm_renderer';
@ -24,7 +23,7 @@ function setup(file: {name: string, contents: string}, transformDts: boolean = f
const decorationAnalyses = const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
const renderer = new EsmRenderer(host, false, null, dir, dir, null); const renderer = new EsmRenderer(host, false, null, dir, dir);
return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses}; return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses};
} }
@ -161,8 +160,9 @@ export class A {}`);
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = decorationAnalyses.get(sourceFile) !.analyzedClasses[0]; const compiledClass =
renderer.addDefinitions(output, analyzedClass, 'SOME DEFINITION TEXT'); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
export class A {} export class A {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
@ -179,9 +179,9 @@ A.decorators = [
() => { () => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -198,9 +198,9 @@ A.decorators = [
() => { () => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -217,9 +217,9 @@ A.decorators = [
() => { () => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM); const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -238,9 +238,9 @@ A.decorators = [
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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -255,9 +255,9 @@ A.decorators = [
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -273,9 +273,9 @@ A.decorators = [
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);

View File

@ -20,7 +20,7 @@ function setup(file: {name: string, contents: string}) {
const decorationAnalyses = const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
const renderer = new EsmRenderer(host, false, null, '', '', null); const renderer = new EsmRenderer(host, false, null, '', '');
return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses}; return {host, program, sourceFile, renderer, decorationAnalyses, switchMarkerAnalyses};
} }
@ -182,8 +182,9 @@ var A = (function() {`);
it('should insert the definitions directly after the class declaration', () => { it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = decorationAnalyses.get(sourceFile) !.analyzedClasses[0]; const compiledClass =
renderer.addDefinitions(output, analyzedClass, 'SOME DEFINITION TEXT'); decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(` expect(output.toString()).toContain(`
function A() {} function A() {}
SOME DEFINITION TEXT SOME DEFINITION TEXT
@ -199,9 +200,9 @@ SOME DEFINITION TEXT
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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -217,9 +218,9 @@ SOME DEFINITION TEXT
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -236,9 +237,9 @@ SOME DEFINITION TEXT
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators[0]; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -257,9 +258,9 @@ SOME DEFINITION TEXT
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 {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -274,9 +275,9 @@ SOME DEFINITION TEXT
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);
@ -292,9 +293,9 @@ SOME DEFINITION TEXT
() => { () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER); const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents); const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass = const compiledClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !; decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !; const decorator = compiledClass.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]);
renderer.removeDecorators(output, decoratorsToRemove); renderer.removeDecorators(output, decoratorsToRemove);

View File

@ -11,20 +11,20 @@ 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, DecorationAnalyses} from '../../src/analysis/decoration_analyzer'; import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Renderer} from '../../src/rendering/renderer'; import {Renderer} from '../../src/rendering/renderer';
class TestRenderer extends Renderer { class TestRenderer extends Renderer {
constructor(host: Esm2015ReflectionHost) { super(host, false, null, '/src', '/dist', null); } constructor(host: Esm2015ReflectionHost) { 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');
} }
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void { addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
output.prepend('\n// ADD CONSTANTS\n'); output.prepend('\n// ADD CONSTANTS\n');
} }
addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string) { addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) {
output.prepend('\n// ADD DEFINITIONS\n'); output.prepend('\n// ADD DEFINITIONS\n');
} }
removeDecorators(output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>) { removeDecorators(output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>) {

View File

@ -12,12 +12,13 @@ import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
export function makeProgram( export function makeProgram(
files: {name: string, contents: string}[], options?: ts.CompilerOptions, files: {name: string, contents: string, isRoot?: boolean}[], options?: ts.CompilerOptions,
host: ts.CompilerHost = new InMemoryHost(), host: ts.CompilerHost = new InMemoryHost(),
checkForErrors: boolean = true): {program: ts.Program, host: ts.CompilerHost} { checkForErrors: boolean = true): {program: ts.Program, host: ts.CompilerHost} {
files.forEach(file => host.writeFile(file.name, file.contents, false, undefined, [])); files.forEach(file => host.writeFile(file.name, file.contents, false, undefined, []));
const rootNames = files.map(file => host.getCanonicalFileName(file.name)); const rootNames =
files.filter(file => file.isRoot !== false).map(file => host.getCanonicalFileName(file.name));
const program = ts.createProgram( const program = ts.createProgram(
rootNames, { rootNames, {
noLib: true, noLib: true,