diff --git a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts index aa9cacc84d..fd24d5c727 100644 --- a/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/module_with_providers_analyzer.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {ReferencesRegistry} from '../../../src/ngtsc/annotations'; import {Reference} from '../../../src/ngtsc/imports'; import {Declaration} from '../../../src/ngtsc/reflection'; -import {NgccReflectionHost} from '../host/ngcc_host'; +import {ModuleWithProvidersFunction, NgccReflectionHost} from '../host/ngcc_host'; import {isDefined} from '../utils'; export interface ModuleWithProvidersInfo { @@ -38,7 +38,7 @@ export class ModuleWithProvidersAnalyzer { rootFiles.forEach(f => { const fns = this.host.getModuleWithProvidersFunctions(f); fns && fns.forEach(fn => { - const dtsFn = this.getDtsDeclaration(fn.declaration); + const dtsFn = this.getDtsDeclarationForFunction(fn); const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) && dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] || null; @@ -82,28 +82,27 @@ export class ModuleWithProvidersAnalyzer { return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined); } - private getDtsDeclaration(fn: ts.SignatureDeclaration) { + private getDtsDeclarationForFunction(fn: ModuleWithProvidersFunction) { let dtsFn: ts.Declaration|null = null; - const containerClass = this.host.getClassSymbol(fn.parent); - const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text; - if (containerClass && fnName) { + const containerClass = fn.container && this.host.getClassSymbol(fn.container); + if (containerClass) { const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration); // Get the declaration of the matching static method dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ? dtsClass.members .find( member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) && - member.name.text === fnName) as ts.Declaration : + member.name.text === fn.name) as ts.Declaration : null; } else { - dtsFn = this.host.getDtsDeclaration(fn); + dtsFn = this.host.getDtsDeclaration(fn.declaration); } if (!dtsFn) { - throw new Error(`Matching type declaration for ${fn.getText()} is missing`); + throw new Error(`Matching type declaration for ${fn.declaration.getText()} is missing`); } if (!isFunctionOrMethod(dtsFn)) { throw new Error( - `Matching type declaration for ${fn.getText()} is not a function: ${dtsFn.getText()}`); + `Matching type declaration for ${fn.declaration.getText()} is not a function: ${dtsFn.getText()}`); } return dtsFn; } diff --git a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts index 674137f7df..8e304b6983 100644 --- a/packages/compiler-cli/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/ngcc/src/host/esm2015_host.ts @@ -374,16 +374,20 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N if (this.isClass(declaration.node)) { this.getMembersOfClass(declaration.node).forEach(member => { if (member.isStatic) { - const info = this.parseForModuleWithProviders(member.node); + const info = this.parseForModuleWithProviders( + member.name, member.node, member.implementation, declaration.node); if (info) { infos.push(info); } } }); } else { - const info = this.parseForModuleWithProviders(declaration.node); - if (info) { - infos.push(info); + if (isNamedDeclaration(declaration.node)) { + const info = + this.parseForModuleWithProviders(declaration.node.name.text, declaration.node); + if (info) { + infos.push(info); + } } } }); @@ -659,8 +663,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * Matching statements will look like: `tslib_1.__decorate(...);`. * @param statement the statement that may contain the call. * @param helperName the name of the helper we are looking for. - * @returns the node that corresponds to the `__decorate(...)` call or null if the statement does - * not match. + * @returns the node that corresponds to the `__decorate(...)` call or null if the statement + * does not match. */ protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null { if (ts.isExpressionStatement(statement)) { @@ -1004,7 +1008,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N /** * Search statements related to the given class for calls to the specified helper. * @param classSymbol the class whose helper calls we are interested in. - * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in. + * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested + * in. * @returns an array of CallExpression nodes for each matching helper call. */ protected getHelperCallsForClass(classSymbol: ts.Symbol, helperName: string): @@ -1099,8 +1104,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N * 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. + * 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. @@ -1126,15 +1131,23 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N } /** - * Parse the given node, to see if it is a function that returns a `ModuleWithProviders` object. - * @param node a node to check to see if it is a function that returns a `ModuleWithProviders` - * object. + * Parse a function/method node (or its implementation), to see if it returns a + * `ModuleWithProviders` object. + * @param name The name of the function. + * @param node the node to check - this could be a function, a method or a variable declaration. + * @param implementation the actual function expression if `node` is a variable declaration. + * @param container the class that contains the function, if it is a method. * @returns info about the function if it does return a `ModuleWithProviders` object; `null` * otherwise. */ - protected parseForModuleWithProviders(node: ts.Node|null): ModuleWithProvidersFunction|null { - const declaration = - node && (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) ? node : null; + protected parseForModuleWithProviders( + name: string, node: ts.Node|null, implementation: ts.Node|null = node, + container: ts.Declaration|null = null): ModuleWithProvidersFunction|null { + const declaration = implementation && + (ts.isFunctionDeclaration(implementation) || ts.isMethodDeclaration(implementation) || + ts.isFunctionExpression(implementation)) ? + implementation : + null; const body = declaration ? this.getDefinitionOfFunction(declaration).body : null; const lastStatement = body && body[body.length - 1]; const returnExpression = @@ -1147,7 +1160,7 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N const ngModule = ngModuleProperty && ts.isPropertyAssignment(ngModuleProperty) && ts.isIdentifier(ngModuleProperty.initializer) && ngModuleProperty.initializer || null; - return ngModule && declaration && {ngModule, declaration}; + return ngModule && declaration && {name, ngModule, declaration, container}; } } diff --git a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts index 4d538e66df..3b932c3c9c 100644 --- a/packages/compiler-cli/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/ngcc/src/host/ngcc_host.ts @@ -24,10 +24,18 @@ export function isSwitchableVariableDeclaration(node: ts.Node): * that return ModuleWithProviders objects. */ export interface ModuleWithProvidersFunction { + /** + * The name of the declared function. + */ + name: string; /** * The declaration of the function that returns the `ModuleWithProviders` object. */ declaration: ts.SignatureDeclaration; + /** + * Declaration of the containing class (if this is a method) + */ + container: ts.Declaration|null; /** * The identifier of the `ngModule` property on the `ModuleWithProviders` object. */ diff --git a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts index c23961455c..e9391d1a7a 100644 --- a/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts @@ -553,7 +553,7 @@ const MODULE_WITH_PROVIDERS_PROGRAM = [ {name: '/src/module', contents: 'export class ExternalModule {}'}, ]; -describe('Fesm2015ReflectionHost', () => { +describe('Esm2015ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { it('should find the decorators on a class', () => { diff --git a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts index 0c1c67ac0a..9fb1cfbdd9 100644 --- a/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts @@ -630,6 +630,71 @@ const TYPINGS_DTS_FILES = [ {name: '/typings/class3.d.ts', contents: `export declare class Class3 {}`}, ]; +const MODULE_WITH_PROVIDERS_PROGRAM = [ + { + name: '/src/functions.js', + contents: ` + import {ExternalModule} from './module'; + + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + return InternalModule; + }()); + export function aNumber() { return 42; } + export function aString() { return 'foo'; } + export function emptyObject() { return {}; } + export function ngModuleIdentifier() { return { ngModule: InternalModule }; } + export function ngModuleWithEmptyProviders() { return { ngModule: InternalModule, providers: [] }; } + export function ngModuleWithProviders() { return { ngModule: InternalModule, providers: [SomeService] }; } + export function onlyProviders() { return { providers: [SomeService] }; } + export function ngModuleNumber() { return { ngModule: 42 }; } + export function ngModuleString() { return { ngModule: 'foo' }; } + export function ngModuleObject() { return { ngModule: { foo: 42 } }; } + export function externalNgModule() { return { ngModule: ExternalModule }; } + export {SomeService, InternalModule}; + ` + }, + { + name: '/src/methods.js', + contents: ` + import {ExternalModule} from './module'; + var SomeService = (function() { + function SomeService() {} + return SomeService; + }()); + + var InternalModule = (function() { + function InternalModule() {} + InternalModule.prototype = { + instanceNgModuleIdentifier: function() { return { ngModule: InternalModule }; }, + instanceNgModuleWithEmptyProviders: function() { return { ngModule: InternalModule, providers: [] }; }, + instanceNgModuleWithProviders: function() { return { ngModule: InternalModule, providers: [SomeService] }; }, + instanceExternalNgModule: function() { return { ngModule: ExternalModule }; }, + }; + InternalModule.aNumber = function() { return 42; }; + InternalModule.aString = function() { return 'foo'; }; + InternalModule.emptyObject = function() { return {}; }; + InternalModule.ngModuleIdentifier = function() { return { ngModule: InternalModule }; }; + InternalModule.ngModuleWithEmptyProviders = function() { return { ngModule: InternalModule, providers: [] }; }; + InternalModule.ngModuleWithProviders = function() { return { ngModule: InternalModule, providers: [SomeService] }; }; + InternalModule.onlyProviders = function() { return { providers: [SomeService] }; }; + InternalModule.ngModuleNumber = function() { return { ngModule: 42 }; }; + InternalModule.ngModuleString = function() { return { ngModule: 'foo' }; }; + InternalModule.ngModuleObject = function() { return { ngModule: { foo: 42 } }; }; + InternalModule.externalNgModule = function() { return { ngModule: ExternalModule }; }; + return InternalModule; + }()); + export {SomeService, InternalModule}; + ` + }, + {name: '/src/module', contents: 'export class ExternalModule {}'}, +]; + describe('Esm5ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1744,4 +1809,46 @@ describe('Esm5ReflectionHost', () => { .toEqual('/typings/class2.d.ts'); }); }); + + describe('getModuleWithProvidersFunctions', () => { + it('should find every exported function that returns an object that looks like a ModuleWithProviders object', + () => { + const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); + const host = new Esm5ReflectionHost(false, srcProgram.getTypeChecker()); + const file = srcProgram.getSourceFile('/src/functions.js') !; + const fns = host.getModuleWithProvidersFunctions(file); + expect(fns.map(info => [info.declaration.name !.getText(), info.ngModule.text])).toEqual([ + ['ngModuleIdentifier', 'InternalModule'], + ['ngModuleWithEmptyProviders', 'InternalModule'], + ['ngModuleWithProviders', 'InternalModule'], + ['externalNgModule', 'ExternalModule'], + ]); + }); + + it('should find every static method on exported classes that return an object that looks like a ModuleWithProviders object', + () => { + const srcProgram = makeTestProgram(...MODULE_WITH_PROVIDERS_PROGRAM); + const host = new Esm5ReflectionHost(false, srcProgram.getTypeChecker()); + const file = srcProgram.getSourceFile('/src/methods.js') !; + const fn = host.getModuleWithProvidersFunctions(file); + expect(fn.map(fn => [fn.declaration.getText(), fn.ngModule.text])).toEqual([ + [ + 'function() { return { ngModule: InternalModule }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: InternalModule, providers: [SomeService] }; }', + 'InternalModule', + ], + [ + 'function() { return { ngModule: ExternalModule }; }', + 'ExternalModule', + ], + ]); + }); + }); });