diff --git a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts index e83a953710..75fb58bfc5 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts @@ -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 extends DecoratedClass { - handler: DecoratorHandler; - 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; + analysis: any; +} + +export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; } + +export interface CompiledFile { + compiledClasses: CompiledClass[]; sourceFile: ts.SourceFile; constantPool: ConstantPool; } -export type DecorationAnalyses = Map; +export type DecorationAnalyses = Map; export const DecorationAnalyses = Map; export interface MatchingHandler { @@ -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; } } diff --git a/packages/compiler-cli/src/ngcc/src/host/decorated_file.ts b/packages/compiler-cli/src/ngcc/src/host/decorated_file.ts deleted file mode 100644 index 0d42307ab0..0000000000 --- a/packages/compiler-cli/src/ngcc/src/host/decorated_file.ts +++ /dev/null @@ -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) {} -} diff --git a/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts b/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts deleted file mode 100644 index db560d02fe..0000000000 --- a/packages/compiler-cli/src/ngcc/src/host/dts_mapper.ts +++ /dev/null @@ -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'); - } -} diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index 50707b679c..a59610ee4b 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -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|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 { - const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); - const map = new Map(); - 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; - } - return undefined; - }) - .filter(isDefined); - - decoratedClasses.forEach(clazz => { - const file = clazz.declaration.getSourceFile(); - if (!map.has(file)) { - map.set(file, new DecoratedFile(file)); + 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); + } + }); + } else if (ts.isClassDeclaration(statement)) { + const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(statement)); + if (decoratedClass) { + classes.push(decoratedClass); } - map.get(file) !.decoratedClasses.push(clazz); - }); - } - 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); + const dtsClass = this.getDtsDeclarationOfClass(clazz); + if (dtsClass) { + return dtsClass.typeParameters ? dtsClass.typeParameters.length : 0; + } + return null; + } - 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; + /** + * 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 { + const dtsClassMap = new Map(); + 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 ///////////// diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index 5ea5c0b95c..a1d5d013ee 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -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 { - const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); - const map = new Map(); - 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 ///////////// diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index 483ade6be0..d8555c9295 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -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; + findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[]; } diff --git a/packages/compiler-cli/src/ngcc/src/main.ts b/packages/compiler-cli/src/ngcc/src/main.ts index 851589d7f0..35f003c2f5 100644 --- a/packages/compiler-cli/src/ngcc/src/main.ts +++ b/packages/compiler-cli/src/ngcc/src/main.ts @@ -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; diff --git a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts index a7955f68a4..a742f8352b 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts @@ -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.`); } diff --git a/packages/compiler-cli/src/ngcc/src/rendering/esm_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/esm_renderer.ts index 087e4de324..a0b3394c74 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/esm_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/esm_renderer.ts @@ -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); diff --git a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts index 9a05bf143e..e20979050f 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/renderer.ts @@ -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(); - 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 renderedFiles; + return this.renderSourceAndMap(sourceFile, input, outputText); + } + + 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): 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: { - path: outputPath, - contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}` - }, - map: {path: outputMapPath, contents: mergedMap.toJSON()} - }; + result.push({ + path: outputPath, + contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}` + }); + result.push({path: outputMapPath, contents: mergedMap.toJSON()}); } + return result; } - // 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}); + protected getTypingsFilesToRender(analyses: DecorationAnalyses): + Map { + const dtsMap = new Map(); + 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 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. */ 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( diff --git a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts index c1abe7709e..2d435ca638 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -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>('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(); + }); + }); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 05e310e056..e9493946b0 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -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'; ` }, { @@ -422,7 +420,7 @@ const DECORATED_FILES = [ D.decorators = [ { type: Directive, args: [{ selector: '[d]' }] } ]; - export { D }; + export {D}; ` } ]; @@ -439,13 +437,38 @@ const ARITY_CLASSES = [ { name: '/typings/class.d.ts', contents: ` - export class NoTypeParam {} - export class OneTypeParam {} - export class TwoTypeParams {} + export declare class NoTypeParam {} + export declare class OneTypeParam {} + export declare class TwoTypeParams {} `, }, ]; +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', - () => { - 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); + 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) !; - 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) !; - expect(primary.decoratedClasses.length).toEqual(1); - const classA = primary.decoratedClasses.find(c => c.name === 'A') !; - expect(classA.name).toEqual('A'); - expect(ts.isClassDeclaration(classA.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') !; - expect(classD.name).toEqual('D'); - expect(ts.isClassDeclaration(classD.declaration)).toBeTruthy(); - expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); - }); + 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'); + }); + }); + }); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index b3ed33794d..4c6ddd0014 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -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,26 +1252,27 @@ describe('Esm5ReflectionHost', () => { }); }); - describe('fileDecoratedFiles()', () => { - it('should return an array of objects for each file that has exported and decorated classes', - () => { - 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'); - expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + 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 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') !; - expect(classD.name).toEqual('D'); - expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); - }); + 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 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']); + }); }); }); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index bfdb94ce89..f0b07ef6bf 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -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(); 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(); 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(); 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(); 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(); 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(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts index 8cc31708b0..88d54d6280 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm5_renderer_spec.ts @@ -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(); 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(); 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(); 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(); 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(); 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(); decoratorsToRemove.set(decorator.node.parent !, [decorator.node]); renderer.removeDecorators(output, decoratorsToRemove); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index 54f69e0d8f..467acbc128 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -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) { diff --git a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts index b963c868a5..36a5b90b0d 100644 --- a/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts +++ b/packages/compiler-cli/src/ngtsc/testing/in_memory_typescript.ts @@ -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,