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 {DecoratedClass} from '../host/decorated_class';
import {DecoratedFile} from '../host/decorated_file';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
export interface AnalyzedClass<A = any, M = any> extends DecoratedClass {
handler: DecoratorHandler<A, M>;
analysis: any;
diagnostics?: ts.Diagnostic[];
compilation: CompileResult[];
export interface AnalyzedFile {
sourceFile: ts.SourceFile;
analyzedClasses: AnalyzedClass[];
}
export interface DecorationAnalysis {
analyzedClasses: AnalyzedClass[];
export interface AnalyzedClass extends DecoratedClass {
diagnostics?: ts.Diagnostic[];
handler: DecoratorHandler<any, any>;
analysis: any;
}
export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; }
export interface CompiledFile {
compiledClasses: CompiledClass[];
sourceFile: ts.SourceFile;
constantPool: ConstantPool;
}
export type DecorationAnalyses = Map<ts.SourceFile, DecorationAnalysis>;
export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>;
export const DecorationAnalyses = Map;
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.
*/
analyzeProgram(program: ts.Program): DecorationAnalyses {
const analyzedFiles = new DecorationAnalyses();
program.getRootFileNames().forEach(fileName => {
const entryPoint = program.getSourceFile(fileName) !;
const decoratedFiles = this.host.findDecoratedFiles(entryPoint);
decoratedFiles.forEach(
decoratedFile =>
analyzedFiles.set(decoratedFile.sourceFile, this.analyzeFile(decoratedFile)));
});
return analyzedFiles;
const decorationAnalyses = new DecorationAnalyses();
const analysedFiles =
program.getSourceFiles().map(sourceFile => this.analyzeFile(sourceFile)).filter(isDefined);
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
compiledFiles.forEach(
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
return decorationAnalyses;
}
/**
* Analyze a decorated file to generate the information about decorated classes that
* should be converted to use ivy definitions.
* @param file The file to be analysed for decorated classes.
* @returns the analysis of the file
*/
protected analyzeFile(file: DecoratedFile): DecorationAnalysis {
const constantPool = new ConstantPool();
const analyzedClasses =
file.decoratedClasses.map(clazz => this.analyzeClass(constantPool, clazz))
.filter(isDefined);
return {
analyzedClasses,
sourceFile: file.sourceFile, constantPool,
};
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
const decoratedClasses = this.host.findDecoratedClasses(sourceFile);
return decoratedClasses.length ? {
sourceFile,
analyzedClasses: decoratedClasses.map(clazz => this.analyzeClass(clazz)).filter(isDefined)
} :
undefined;
}
protected analyzeClass(pool: ConstantPool, clazz: DecoratedClass): AnalyzedClass|undefined {
protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
const matchingHandlers = this.handlers
.map(handler => ({
handler,
match: handler.detect(clazz.declaration, clazz.decorators),
}))
.map(handler => {
const match =
handler.detect(clazz.declaration, clazz.decorators);
return {handler, match};
})
.filter(isMatchingHandler);
if (matchingHandlers.length > 1) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}
if (matchingHandlers.length === 0) {
return undefined;
return null;
}
const {handler, match} = matchingHandlers[0];
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)) {
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
*/
import {readFileSync} from 'fs';
import * as ts from 'typescript';
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 {DecoratedClass} from './decorated_class';
import {DecoratedFile} from './decorated_file';
import {DtsMapper} from './dts_mapper';
import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String;
@ -51,8 +48,14 @@ export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
* a static method called `ctorParameters`.
*/
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);
this.dtsClassMap = (dtsRootFileName && dtsProgram) ?
this.computeDtsClassMap(dtsRootFileName, dtsProgram) :
null;
}
/**
@ -73,12 +76,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (!symbol) {
return null;
}
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
if (decoratorsProperty) {
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
} else {
return this.getClassDecoratorsFromHelperCall(symbol);
}
return this.getDecoratorsOfSymbol(symbol);
}
/**
@ -206,10 +204,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
if (ts.isClassDeclaration(declaration)) {
return declaration.name && this.checker.getSymbolAtLocation(declaration.name);
}
if (ts.isVariableDeclaration(declaration) && declaration.initializer &&
ts.isClassExpression(declaration.initializer)) {
return declaration.initializer.name &&
this.checker.getSymbolAtLocation(declaration.initializer.name);
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
declaration = declaration.initializer;
}
if (ts.isClassExpression(declaration)) {
return declaration.name && this.checker.getSymbolAtLocation(declaration.name);
}
return undefined;
}
@ -299,46 +298,29 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id);
}
/*
* 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.
/**
* Find all the classes that contain decorations in a given file.
* @param sourceFile The source file to search for decorated classes.
* @returns An array of decorated classes.
*/
findDecoratedFiles(entryPoint: ts.SourceFile): Map<ts.SourceFile, DecoratedFile> {
const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint);
const map = new Map<ts.SourceFile, DecoratedFile>();
if (moduleSymbol) {
const exportedSymbols =
this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker));
const exportedDeclarations =
exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined);
const decoratedClasses =
exportedDeclarations
.map(declaration => {
if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) {
const name = declaration.name && ts.isIdentifier(declaration.name) ?
declaration.name.text :
undefined;
const decorators = this.getDecoratorsOfDeclaration(declaration);
return decorators && isDefined(name) ?
new DecoratedClass(name, declaration, decorators) :
undefined;
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] {
const classes: DecoratedClass[] = [];
sourceFile.statements.map(statement => {
if (ts.isVariableStatement(statement)) {
statement.declarationList.declarations.forEach(declaration => {
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration));
if (decoratedClass) {
classes.push(decoratedClass);
}
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);
});
} else if (ts.isClassDeclaration(statement)) {
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(statement));
if (decoratedClass) {
classes.push(decoratedClass);
}
return map;
}
});
return classes;
}
/**
@ -348,21 +330,38 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* is not a class or has an unknown number of type parameters.
*/
getGenericArityOfClass(clazz: ts.Declaration): number|null {
if (this.dtsMapper && ts.isClassDeclaration(clazz) && clazz.name) {
const sourcePath = clazz.getSourceFile();
const dtsPath = this.dtsMapper.getDtsFileNameFor(sourcePath.fileName);
const dtsContents = readFileSync(dtsPath, 'utf8');
// TODO: investigate caching parsed .d.ts files as they're needed for several different
// 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];
if (ts.isClassDeclaration(stmt) && stmt.name !== undefined &&
stmt.name.text === clazz.name.text) {
return stmt.typeParameters ? stmt.typeParameters.length : 0;
const dtsClass = this.getDtsDeclarationOfClass(clazz);
if (dtsClass) {
return dtsClass.typeParameters ? dtsClass.typeParameters.length : 0;
}
return null;
}
/**
* Take an exported declaration of a class (maybe downleveled to a variable) and look up the
* declaration of its type in a separate .d.ts tree.
*
* 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;
@ -371,6 +370,25 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
///////////// 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.
* @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 name: string|null = null;
let nameNode: ts.Identifier|null = null;
let type = null;
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);
}
const type: ts.TypeNode = (node as any).type || null;
return {
node,
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.
*/
isFromCore(decorator: Decorator): boolean {
protected isFromCore(decorator: Decorator): boolean {
if (this.isCore) {
return !decorator.import || /^\./.test(decorator.import.from);
} else {
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 /////////////

View File

@ -10,10 +10,8 @@ import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host';
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';
@ -125,41 +123,6 @@ export class Esm5ReflectionHost extends Esm2015ReflectionHost {
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 /////////////

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {ReflectionHost} from '../../../ngtsc/host';
import {DecoratedFile} from './decorated_file';
import {DecoratedClass} from './decorated_class';
export const PRE_R3_MARKER = '__PRE_R3__';
export const POST_R3_MARKER = '__POST_R3__';
@ -40,10 +40,9 @@ export interface NgccReflectionHost extends ReflectionHost {
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
/**
* 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.
* Find all the classes that contain decorations in a given file.
* @param sourceFile The source file to search for decorated classes.
* @returns An array of decorated classes.
*/
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
*/
import * as path from 'canonical-path';
import {existsSync, lstatSync, readFileSync, readdirSync} from 'fs';
import * as yargs from 'yargs';
import {DependencyHost} from './packages/dependency_host';
@ -48,8 +47,14 @@ export function mainNgcc(args: string[]): number {
try {
const {entryPoints} = finder.findEntryPoints(sourcePath);
entryPoints.forEach(
entryPoint => formats.forEach(format => transformer.transform(entryPoint, format)));
entryPoints.forEach(entryPoint => {
// 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) {
console.error(e.stack);
return 1;

View File

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

View File

@ -7,17 +7,16 @@
*/
import * as ts from 'typescript';
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 {AnalyzedClass} from '../analysis/decoration_analyzer';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {Renderer} from './renderer';
export class EsmRenderer extends Renderer {
constructor(
protected host: NgccReflectionHost, protected isCore: boolean,
protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string,
protected targetPath: string, dtsMapper: DtsMapper|null) {
super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath, dtsMapper);
protected targetPath: string) {
super(host, isCore, rewriteCoreImportsTo, sourcePath, targetPath);
}
/**
@ -45,10 +44,10 @@ export class EsmRenderer extends Renderer {
/**
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(analyzedClass.declaration);
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
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();
output.appendLeft(insertionPoint, '\n' + definitions);

View File

@ -14,13 +14,12 @@ import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/host';
import {DtsFileTransformer} from '../../../ngtsc/transform';
import {translateStatement} from '../../../ngtsc/translator';
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
import {translateStatement, translateType} from '../../../ngtsc/translator';
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 {IMPORT_PREFIX} from '../constants';
import {DtsMapper} from '../host/dts_mapper';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
interface SourceMapInfo {
@ -29,20 +28,6 @@ interface SourceMapInfo {
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.
*/
@ -57,6 +42,11 @@ export interface FileInfo {
contents: string;
}
interface DtsClassInfo {
dtsDeclaration: ts.Declaration;
compilation: CompileResult[];
}
/**
* A base-class for rendering an `AnalyzedFile`.
*
@ -67,39 +57,38 @@ export abstract class Renderer {
constructor(
protected host: NgccReflectionHost, protected isCore: boolean,
protected rewriteCoreImportsTo: ts.SourceFile|null, protected sourcePath: string,
protected targetPath: string, protected dtsMapper: DtsMapper|null) {}
protected targetPath: string) {}
renderProgram(
program: ts.Program, decorationAnalyses: DecorationAnalyses,
switchMarkerAnalyses: SwitchMarkerAnalyses): FileInfo[] {
const renderedFiles: FileInfo[] = [];
// Transform the source files, source maps and typings files.
// Transform the source files.
program.getSourceFiles().map(sourceFile => {
const decorationAnalysis = decorationAnalyses.get(sourceFile);
const compiledFile = decorationAnalyses.get(sourceFile);
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
if (decorationAnalysis || switchMarkerAnalysis) {
const targetPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
renderedFiles.push(
...this.renderFile(sourceFile, decorationAnalysis, switchMarkerAnalysis, targetPath));
}
if (decorationAnalyses) {
renderedFiles.push(...this.renderTypings(decorationAnalyses));
if (compiledFile || switchMarkerAnalysis) {
renderedFiles.push(...this.renderFile(sourceFile, compiledFile, switchMarkerAnalysis));
}
});
// Transform the .d.ts files
const dtsFiles = this.getTypingsFilesToRender(decorationAnalyses);
dtsFiles.forEach((classes, file) => renderedFiles.push(...this.renderDtsFile(file, classes)));
return renderedFiles;
}
/**
* 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.
*/
renderFile(
sourceFile: ts.SourceFile, decorationAnalysis: DecorationAnalysis|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined, targetPath: string): FileInfo[] {
sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined): FileInfo[] {
const input = this.extractSourceMap(sourceFile);
const outputText = new MagicString(input.source);
@ -108,46 +97,59 @@ export abstract class Renderer {
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
}
if (decorationAnalysis) {
if (compiledFile) {
const importManager =
new NgccImportManager(!this.rewriteCoreImportsTo, this.isCore, IMPORT_PREFIX);
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decorationAnalysis.analyzedClasses.forEach(clazz => {
const renderedDefinition =
renderDefinitions(decorationAnalysis.sourceFile, clazz, importManager);
compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
this.addDefinitions(outputText, clazz, renderedDefinition);
this.trackDecorators(clazz.decorators, decoratorsToRemove);
});
this.addConstants(
outputText,
renderConstantPool(
decorationAnalysis.sourceFile, decorationAnalysis.constantPool, importManager),
decorationAnalysis.sourceFile);
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
compiledFile.sourceFile);
this.addImports(
outputText, importManager.getAllImports(
decorationAnalysis.sourceFile.fileName, this.rewriteCoreImportsTo));
outputText,
importManager.getAllImports(compiledFile.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);
}
const {source, map} = this.renderSourceAndMap(sourceFile, input, outputText, targetPath);
const renderedFiles = [source];
if (map) {
renderedFiles.push(map);
return this.renderSourceAndMap(sourceFile, input, outputText);
}
return renderedFiles;
renderDtsFile(dtsFile: ts.SourceFile, dtsClasses: DtsClassInfo[]): FileInfo[] {
const input = this.extractSourceMap(dtsFile);
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):
void;
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void;
protected abstract addDefinitions(
output: MagicString, analyzedClass: AnalyzedClass, definitions: string): void;
output: MagicString, compiledClass: CompiledClass, definitions: string): void;
protected abstract removeDecorators(
output: MagicString, decoratorsToRemove: Map<ts.Node, ts.Node[]>): void;
protected abstract rewriteSwitchableDeclarations(
@ -219,8 +221,8 @@ export abstract class Renderer {
* with an appropriate source-map comment pointing to the merged source-map.
*/
protected renderSourceAndMap(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString,
outputPath: string): RenderResult {
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
const outputPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
const outputMapPath = `${outputPath}.map`;
const outputMap = output.generateMap({
source: sourceFile.fileName,
@ -235,41 +237,34 @@ export abstract class Renderer {
const mergedMap =
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
const result: FileInfo[] = [];
if (input.isInline) {
return {
source: {path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`},
map: null
};
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
} else {
return {
source: {
result.push({
path: outputPath,
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
},
map: {path: outputMapPath, contents: mergedMap.toJSON()}
};
}
}
// TODO(gkalpak): What about `.d.ts` source maps? (See
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-9.html#new---declarationmap.)
renderTypings(decorationAnalyses: DecorationAnalyses): FileInfo[] {
const renderedFiles: FileInfo[] = [];
if (this.dtsMapper) {
const dtsTransformer = new DtsFileTransformer(this.rewriteCoreImportsTo, IMPORT_PREFIX);
decorationAnalyses.forEach((analysis, sourceFile) => {
const sourceFileName = sourceFile.fileName;
const dtsFileName = this.dtsMapper !.getDtsFileNameFor(sourceFileName);
const dtsContents = readFileSync(dtsFileName, 'utf8');
analysis.analyzedClasses.forEach(analyzedClass => dtsTransformer.recordStaticField(analyzedClass.name, analyzedClass.compilation));
const newDtsFileName = resolve(this.targetPath, relative(this.sourcePath, dtsFileName));
const newDtsContents = dtsTransformer.transform(dtsContents, sourceFileName);
renderedFiles.push({path: newDtsFileName, contents: newDtsContents});
});
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
}
return result;
}
return renderedFiles;
// }
protected getTypingsFilesToRender(analyses: DecorationAnalyses):
Map<ts.SourceFile, DtsClassInfo[]> {
const dtsMap = new Map<ts.SourceFile, DtsClassInfo[]>();
analyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclarationOfClass(compiledClass.declaration);
if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile();
const classes = dtsMap.get(dtsFile) || [];
classes.push({dtsDeclaration, compilation: compiledClass.compilation});
dtsMap.set(dtsFile, classes);
}
});
});
return dtsMap;
}
}
@ -321,11 +316,11 @@ export function renderConstantPool(
* @param imports An object that tracks the imports that are needed by the rendered definitions.
*/
export function renderDefinitions(
sourceFile: ts.SourceFile, analyzedClass: AnalyzedClass, imports: NgccImportManager): string {
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: NgccImportManager): string {
const printer = ts.createPrinter();
const name = (analyzedClass.declaration as ts.NamedDeclaration).name !;
const name = (compiledClass.declaration as ts.NamedDeclaration).name !;
const definitions =
analyzedClass.compilation
compiledClass.compilation
.map(
c => c.statements.map(statement => translateStatement(statement, imports))
.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() {
const handler = jasmine.createSpyObj<DecoratorHandler<any, any>>('TestDecoratorHandler', [
'detect',
@ -79,9 +107,9 @@ describe('DecorationAnalyzer', () => {
it('should return an object containing the classes that were analyzed', () => {
const file = program.getSourceFile(TEST_PROGRAM.name) !;
const analysis = result.get(file) !;
expect(analysis.analyzedClasses.length).toEqual(1);
expect(analysis.analyzedClasses[0].name).toEqual('MyComponent');
const compiledFile = result.get(file) !;
expect(compiledFile.compiledClasses.length).toEqual(1);
expect(compiledFile.compiledClasses[0].name).toEqual('MyComponent');
});
it('should analyze and compile the classes that are detected', () => {
@ -91,5 +119,41 @@ describe('DecorationAnalyzer', () => {
expect(testHandler.compile).toHaveBeenCalledTimes(1);
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
*/
import * as fs from 'fs';
import * as ts from 'typescript';
import {ClassMemberKind, Import} from '../../../ngtsc/host';
import {DtsMapper} from '../../src/host/dts_mapper';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getDeclaration, makeProgram} from '../helpers/utils';
@ -399,6 +397,7 @@ const DECORATED_FILES = [
name: '/primary.js',
contents: `
import {Directive} from '@angular/core';
import {D} from '/secondary';
class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
@ -411,7 +410,6 @@ const DECORATED_FILES = [
];
class C {}
export { A, x, C };
export { D } from '/secondary';
`
},
{
@ -439,13 +437,38 @@ const ARITY_CLASSES = [
{
name: '/typings/class.d.ts',
contents: `
export class NoTypeParam {}
export class OneTypeParam<T> {}
export class TwoTypeParams<T, K> {}
export declare class NoTypeParam {}
export declare class OneTypeParam<T> {}
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('getDecoratorsOfDeclaration()', () => {
@ -1186,12 +1209,10 @@ describe('Fesm2015ReflectionHost', () => {
describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => {
// Mock out reading the `d.ts` file from disk
const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(ARITY_CLASSES[1].contents);
const dtsProgram = makeProgram(ARITY_CLASSES[1]);
const program = makeProgram(ARITY_CLASSES[0]);
const dtsMapper = new DtsMapper('/src', '/typings');
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsMapper);
const host = new Esm2015ReflectionHost(
false, program.getTypeChecker(), ARITY_CLASSES[1].name, dtsProgram);
const noTypeParamClass =
getDeclaration(program, '/src/class.js', 'NoTypeParam', ts.isClassDeclaration);
expect(host.getGenericArityOfClass(noTypeParamClass)).toBe(0);
@ -1217,30 +1238,95 @@ describe('Fesm2015ReflectionHost', () => {
});
});
describe('findDecoratedFiles()', () => {
it('should return an array of objects for each file that has exported and decorated classes',
() => {
describe('findDecoratedClasses()', () => {
it('should return an array of all decorated classes in the given source file', () => {
const program = makeProgram(...DECORATED_FILES);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const primaryFile = program.getSourceFile(DECORATED_FILES[0].name) !;
const secondaryFile = program.getSourceFile(DECORATED_FILES[1].name) !;
const decoratedFiles = host.findDecoratedFiles(primaryFile);
expect(decoratedFiles.size).toEqual(2);
const primary = decoratedFiles.get(primaryFile) !;
expect(primary.decoratedClasses.length).toEqual(1);
const classA = primary.decoratedClasses.find(c => c.name === 'A') !;
expect(classA.name).toEqual('A');
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 secondary = decoratedFiles.get(secondaryFile) !;
expect(secondary.decoratedClasses.length).toEqual(1);
const classD = secondary.decoratedClasses.find(c => c.name === 'D') !;
const secondaryDecoratedClasses = host.findDecoratedClasses(secondaryFile) !;
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(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',
contents: `
import {Directive} from '@angular/core';
import { D } from '/secondary';
var A = (function() {
function A() {}
A.decorators = [
@ -453,7 +454,6 @@ const DECORATED_FILES = [
return C;
});
export { A, x, C };
export { D } from '/secondary';
`
},
{
@ -1252,24 +1252,25 @@ describe('Esm5ReflectionHost', () => {
});
});
describe('fileDecoratedFiles()', () => {
it('should return an array of objects for each file that has exported and decorated classes',
() => {
describe('findDecoratedClasses()', () => {
it('should return an array of all decorated classes in the given source file', () => {
const program = makeProgram(...DECORATED_FILES);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
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');
const primaryDecoratedClasses = host.findDecoratedClasses(primary);
expect(primaryDecoratedClasses.length).toEqual(2);
const classA = primaryDecoratedClasses.find(c => c.name === 'A') !;
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(classB.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = program.getSourceFile(DECORATED_FILES[1].name) !;
const secondaryClasses = decoratedFiles.get(secondary) !.decoratedClasses;
expect(secondaryClasses.length).toEqual(1);
const classD = secondaryClasses.find(c => c.name === 'D') !;
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 {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {DtsMapper} from '../../src/host/dts_mapper';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {EsmRenderer} from '../../src/rendering/esm_renderer';
@ -24,7 +23,7 @@ function setup(file: {name: string, contents: string}, transformDts: boolean = f
const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).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};
}
@ -161,8 +160,9 @@ export class A {}`);
it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const analyzedClass = decorationAnalyses.get(sourceFile) !.analyzedClasses[0];
renderer.addDefinitions(output, analyzedClass, 'SOME DEFINITION TEXT');
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`
export class A {}
SOME DEFINITION TEXT
@ -179,9 +179,9 @@ A.decorators = [
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators[0];
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
@ -198,9 +198,9 @@ A.decorators = [
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators[0];
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
@ -217,9 +217,9 @@ A.decorators = [
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators[0];
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
@ -238,9 +238,9 @@ A.decorators = [
it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'A') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
@ -255,9 +255,9 @@ A.decorators = [
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'B') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
@ -273,9 +273,9 @@ A.decorators = [
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const analyzedClass =
decorationAnalyses.get(sourceFile) !.analyzedClasses.find(c => c.name === 'C') !;
const decorator = analyzedClass.decorators.find(d => d.name === 'Directive') !;
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);

View File

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

View File

@ -11,20 +11,20 @@ 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, DecorationAnalyses} from '../../src/analysis/decoration_analyzer';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Renderer} from '../../src/rendering/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}[]) {
output.prepend('\n// ADD IMPORTS\n');
}
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
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');
}
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';
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(),
checkForErrors: boolean = true): {program: ts.Program, host: ts.CompilerHost} {
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(
rootNames, {
noLib: true,