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 a59610ee4b..4ef22cb3b3 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -356,12 +356,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N 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 this.dtsClassMap.get(declaration.name.text) || null; } } return null; @@ -993,31 +988,37 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } } + /** + * Extract all the class declarations from the dtsTypings program, storing them in a map + * where the key is the declared name of the class and the value is the declaration itself. + * + * It is possible for there to be multiple class declarations with the same local name. + * Only the first declaration with a given name is added to the map; subsequent classes will be + * ignored. + * + * We are most interested in classes that are publicly exported from the entry point, so these are + * added to the map first, to ensure that they are not ignored. + * + * @param dtsRootFileName The filename of the entry-point to the `dtsTypings` program. + * @param dtsProgram The program containing all the typings files. + * @returns a map of class names to class declarations. + */ 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); - } - } - }); + + // First add all the classes that are publicly exported from the entry-point + const rootFile = dtsProgram.getSourceFile(dtsRootFileName); + if (!rootFile) { + throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`); } + collectExportedClasses(checker, dtsClassMap, rootFile); + + // Now add any additional classes that are exported from individual dts files, + // but are not publicly exported from the entry-point. + dtsProgram.getSourceFiles().forEach( + sourceFile => { collectExportedClasses(checker, dtsClassMap, sourceFile); }); return dtsClassMap; } } @@ -1151,3 +1152,28 @@ function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.I } return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null; } + +/** + * Search a source file for exported classes, storing them in the provided `dtsClassMap`. + * @param checker The typechecker for the source program. + * @param dtsClassMap The map in which to store the collected exported classes. + * @param srcFile The source file to search for exported classes. + */ +function collectExportedClasses( + checker: ts.TypeChecker, dtsClassMap: Map, + srcFile: ts.SourceFile): void { + const srcModule = srcFile && checker.getSymbolAtLocation(srcFile); + const moduleExports = srcModule && checker.getExportsOfModule(srcModule); + if (moduleExports) { + moduleExports.forEach(exportedSymbol => { + if (exportedSymbol.flags & ts.SymbolFlags.Alias) { + exportedSymbol = checker.getAliasedSymbol(exportedSymbol); + } + const declaration = exportedSymbol.valueDeclaration; + const name = exportedSymbol.name; + if (declaration && ts.isClassDeclaration(declaration) && !dtsClassMap.has(name)) { + dtsClassMap.set(name, declaration); + } + }); + } +} 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 e9493946b0..2878872ec2 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 @@ -445,9 +445,14 @@ const ARITY_CLASSES = [ ]; const TYPINGS_SRC_FILES = [ - {name: '/src/index.js', contents: `export * from './class1'; export * from './class2';`}, + { + name: '/src/index.js', + contents: + `import {InternalClass} from './internal'; 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/internal.js', contents: 'export class InternalClass {}\nexport class Class2 {}'}, {name: '/src/missing-class.js', contents: 'export class MissingClass2 {}'}, { name: '/src/flat-file.js', contents: @@ -456,7 +461,11 @@ const TYPINGS_SRC_FILES = [ ]; const TYPINGS_DTS_FILES = [ - {name: '/typings/index.d.ts', contents: `export * from './class1'; export * from './class2';`}, + { + name: '/typings/index.d.ts', + contents: + `import {InternalClass} from './internal'; export * from './class1'; export * from './class2';` + }, { name: '/typings/class1.d.ts', contents: `export declare class Class1 {}\nexport declare class OtherClass {}` @@ -466,6 +475,10 @@ const TYPINGS_DTS_FILES = [ contents: `export declare class Class2 {}\nexport declare interface SomeInterface {}\nexport {Class3 as xClass3} from './class3';` }, + { + name: '/typings/internal.d.ts', + contents: `export declare class InternalClass {}\nexport declare class Class2 {}` + }, {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, ]; @@ -1277,7 +1290,7 @@ describe('Fesm2015ReflectionHost', () => { 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', () => { + it('should return null 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 = @@ -1285,12 +1298,10 @@ describe('Fesm2015ReflectionHost', () => { 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'); + expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null); }); - it('should throw an error if there is no matching dts file', () => { + it('should return null if there is no matching dts file', () => { const srcProgram = makeProgram(...TYPINGS_SRC_FILES); const dtsProgram = makeProgram(...TYPINGS_DTS_FILES); const missingClass = getDeclaration( @@ -1298,9 +1309,7 @@ describe('Fesm2015ReflectionHost', () => { 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'); + expect(host.getDtsDeclarationOfClass(missingClass)).toBe(null); }); it('should find the dts file that contains a matching class declaration, even if the source files do not match', @@ -1327,6 +1336,37 @@ describe('Fesm2015ReflectionHost', () => { const dtsDeclaration = host.getDtsDeclarationOfClass(class3); expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class3.d.ts'); }); - }); + it('should find the dts file that contains a matching class declaration, even if the class is not publicly exported', + () => { + const srcProgram = makeProgram(...TYPINGS_SRC_FILES); + const dtsProgram = makeProgram(...TYPINGS_DTS_FILES); + const internalClass = + getDeclaration(srcProgram, '/src/internal.js', 'InternalClass', ts.isClassDeclaration); + const host = new Esm2015ReflectionHost( + false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram); + + const dtsDeclaration = host.getDtsDeclarationOfClass(internalClass); + expect(dtsDeclaration !.getSourceFile().fileName).toEqual('/typings/internal.d.ts'); + }); + + it('should prefer the publicly exported class if there are multiple classes with the same name', + () => { + const srcProgram = makeProgram(...TYPINGS_SRC_FILES); + const dtsProgram = makeProgram(...TYPINGS_DTS_FILES); + const class2 = + getDeclaration(srcProgram, '/src/class2.js', 'Class2', ts.isClassDeclaration); + const internalClass2 = + getDeclaration(srcProgram, '/src/internal.js', 'Class2', ts.isClassDeclaration); + const host = new Esm2015ReflectionHost( + false, srcProgram.getTypeChecker(), TYPINGS_DTS_FILES[0].name, dtsProgram); + + const class2DtsDeclaration = host.getDtsDeclarationOfClass(class2); + expect(class2DtsDeclaration !.getSourceFile().fileName).toEqual('/typings/class2.d.ts'); + + const internalClass2DtsDeclaration = host.getDtsDeclarationOfClass(internalClass2); + expect(internalClass2DtsDeclaration !.getSourceFile().fileName) + .toEqual('/typings/class2.d.ts'); + }); + }); });