diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 54ccce6ae2..aac8f1bfef 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -64,12 +64,16 @@ export class NgModuleDecoratorHandler implements DecoratorHandler this._extractModuleFromModuleWithProvidersFn(node)); imports = resolveTypeList(importsMeta, 'imports'); } let exports: Reference[] = []; if (ngModule.has('exports')) { - const exportsMeta = staticallyResolve(ngModule.get('exports') !, this.checker); + const exportsMeta = staticallyResolve( + ngModule.get('exports') !, this.checker, + node => this._extractModuleFromModuleWithProvidersFn(node)); exports = resolveTypeList(exportsMeta, 'exports'); } @@ -125,6 +129,46 @@ export class NgModuleDecoratorHandler implements DecoratorHandler()}); +export function staticallyResolve( + node: ts.Expression, checker: ts.TypeChecker, + foreignFunctionResolver?: (node: ts.FunctionDeclaration | ts.MethodDeclaration) => + ts.Expression | null): ResolvedValue { + return new StaticInterpreter(checker).visit(node, { + absoluteModuleName: null, + scope: new Map(), foreignFunctionResolver, + }); } interface BinaryOperatorDef { @@ -226,6 +239,7 @@ const UNARY_OPERATORS = new Map any>([ interface Context { absoluteModuleName: string|null; scope: Scope; + foreignFunctionResolver?(node: ts.FunctionDeclaration|ts.MethodDeclaration): ts.Expression|null; } class StaticInterpreter { @@ -472,6 +486,10 @@ class StaticInterpreter { } else if (lhs instanceof Reference) { const ref = lhs.node; if (ts.isClassDeclaration(ref)) { + let absoluteModuleName = context.absoluteModuleName; + if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { + absoluteModuleName = lhs.moduleName || absoluteModuleName; + } let value: ResolvedValue = undefined; const member = ref.members.filter(member => isStatic(member)) @@ -482,7 +500,7 @@ class StaticInterpreter { if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) { value = this.visitExpression(member.initializer, context); } else if (ts.isMethodDeclaration(member)) { - value = new NodeReference(member); + value = new NodeReference(member, absoluteModuleName); } } return value; @@ -495,13 +513,35 @@ class StaticInterpreter { const lhs = this.visitExpression(node.expression, context); if (!(lhs instanceof Reference)) { throw new Error(`attempting to call something that is not a function: ${lhs}`); - } else if (!isFunctionOrMethodDeclaration(lhs.node) || !lhs.node.body) { + } else if (!isFunctionOrMethodDeclaration(lhs.node)) { throw new Error( `calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]}`); } const fn = lhs.node; - const body = fn.body as ts.Block; + + // If the function is foreign (declared through a d.ts file), attempt to resolve it with the + // foreignFunctionResolver, if one is specified. + if (fn.body === undefined) { + let expr: ts.Expression|null = null; + if (context.foreignFunctionResolver) { + expr = context.foreignFunctionResolver(fn); + } + if (expr === null) { + throw new Error(`could not resolve foreign function declaration`); + } + + // If the function is declared in a different file, resolve the foreign function expression + // using the absolute module name of that file (if any). + let absoluteModuleName: string|null = context.absoluteModuleName; + if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) { + absoluteModuleName = lhs.moduleName || absoluteModuleName; + } + + return this.visitExpression(expr, {...context, absoluteModuleName}); + } + + const body = fn.body; if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) { throw new Error('Function body must have a single return statement only.'); } diff --git a/packages/compiler-cli/test/ngtsc/fake_core/index.ts b/packages/compiler-cli/test/ngtsc/fake_core/index.ts index beceb52459..7999ab1cc4 100644 --- a/packages/compiler-cli/test/ngtsc/fake_core/index.ts +++ b/packages/compiler-cli/test/ngtsc/fake_core/index.ts @@ -26,3 +26,4 @@ export const Inject = callableParamDecorator(); export const Self = callableParamDecorator(); export const SkipSelf = callableParamDecorator(); export const Optional = callableParamDecorator(); +export type ModuleWithProviders = any; diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index e690129325..3046f5dbc6 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -344,4 +344,30 @@ describe('ngtsc behavioral tests', () => { const dtsContents = getContents('test.d.ts'); expect(dtsContents).toContain('i0.ɵNgModuleDef'); }); + + it('should unwrap a ModuleWithProviders functoin if a generic type is provided for it', () => { + writeConfig(); + write(`test.ts`, ` + import {NgModule} from '@angular/core'; + import {RouterModule} from 'router'; + + @NgModule({imports: [RouterModule.forRoot()]}) + export class TestModule {} + `); + + write('node_modules/router/index.d.ts', ` + import {ModuleWithProviders} from '@angular/core'; + + declare class RouterModule { + static forRoot(): ModuleWithProviders; + } + `); + + const exitCode = main(['-p', basePath], errorSpy); + expect(errorSpy).not.toHaveBeenCalled(); + expect(exitCode).toBe(0); + const dtsContents = getContents('test.d.ts'); + expect(dtsContents).toContain(`import * as i1 from 'router';`); + expect(dtsContents).toContain('i0.ɵNgModuleDef'); + }); }); diff --git a/packages/core/src/metadata/ng_module.ts b/packages/core/src/metadata/ng_module.ts index ed4634a25c..78de8a4347 100644 --- a/packages/core/src/metadata/ng_module.ts +++ b/packages/core/src/metadata/ng_module.ts @@ -73,10 +73,11 @@ export interface NgModuleDef { /** * A wrapper around an NgModule that associates it with the providers. * - * + * @param T the module type. In Ivy applications, this must be explicitly + * provided. */ -export interface ModuleWithProviders { - ngModule: Type; +export interface ModuleWithProviders { + ngModule: Type; providers?: Provider[]; } diff --git a/packages/forms/src/form_providers.ts b/packages/forms/src/form_providers.ts index 586a13b506..a0759331f8 100644 --- a/packages/forms/src/form_providers.ts +++ b/packages/forms/src/form_providers.ts @@ -38,7 +38,7 @@ export class FormsModule { export class ReactiveFormsModule { static withConfig(opts: { /** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never' | 'once' | 'always' - }): ModuleWithProviders { + }): ModuleWithProviders { return { ngModule: ReactiveFormsModule, providers: [{ diff --git a/packages/platform-browser/src/browser.ts b/packages/platform-browser/src/browser.ts index 8123f422ee..be4821570b 100644 --- a/packages/platform-browser/src/browser.ts +++ b/packages/platform-browser/src/browser.ts @@ -112,7 +112,7 @@ export class BrowserModule { * * @experimental */ - static withServerTransition(params: {appId: string}): ModuleWithProviders { + static withServerTransition(params: {appId: string}): ModuleWithProviders { return { ngModule: BrowserModule, providers: [ diff --git a/packages/router/src/router_module.ts b/packages/router/src/router_module.ts index 73995ef7d3..7566ee1400 100644 --- a/packages/router/src/router_module.ts +++ b/packages/router/src/router_module.ts @@ -155,7 +155,7 @@ export class RouterModule { * * `paramsInheritanceStrategy` defines how the router merges params, data and resolved data * from parent to child routes. */ - static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders { + static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders { return { ngModule: RouterModule, providers: [ @@ -193,7 +193,7 @@ export class RouterModule { /** * Creates a module with all the router directives and a provider registering routes. */ - static forChild(routes: Routes): ModuleWithProviders { + static forChild(routes: Routes): ModuleWithProviders { return {ngModule: RouterModule, providers: [provideRoutes(routes)]}; } } diff --git a/packages/router/testing/src/router_testing_module.ts b/packages/router/testing/src/router_testing_module.ts index ef4c5ab387..4f2935a483 100644 --- a/packages/router/testing/src/router_testing_module.ts +++ b/packages/router/testing/src/router_testing_module.ts @@ -180,7 +180,8 @@ export function setupTestingRouter( ] }) export class RouterTestingModule { - static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders { + static withRoutes(routes: Routes, config?: ExtraOptions): + ModuleWithProviders { return { ngModule: RouterTestingModule, providers: [ diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index 09052c005c..dc2ebf8627 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -510,8 +510,8 @@ export declare class ModuleWithComponentFactories { constructor(ngModuleFactory: NgModuleFactory, componentFactories: ComponentFactory[]); } -export interface ModuleWithProviders { - ngModule: Type; +export interface ModuleWithProviders { + ngModule: Type; providers?: Provider[]; } diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index 4af0d6af73..9defb7acc0 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -458,7 +458,7 @@ export declare class RadioControlValueAccessor implements ControlValueAccessor, export declare class ReactiveFormsModule { static withConfig(opts: { warnOnNgModelWithFormControl: 'never' | 'once' | 'always'; - }): ModuleWithProviders; + }): ModuleWithProviders; } export declare class RequiredValidator implements Validator { diff --git a/tools/public_api_guard/platform-browser/platform-browser.d.ts b/tools/public_api_guard/platform-browser/platform-browser.d.ts index 52231287f2..df3c1c135a 100644 --- a/tools/public_api_guard/platform-browser/platform-browser.d.ts +++ b/tools/public_api_guard/platform-browser/platform-browser.d.ts @@ -2,7 +2,7 @@ export declare class BrowserModule { constructor(parentModule: BrowserModule | null); /** @experimental */ static withServerTransition(params: { appId: string; - }): ModuleWithProviders; + }): ModuleWithProviders; } /** @experimental */ diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index 8965f82018..6575a15279 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -414,8 +414,8 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy { export declare class RouterModule { constructor(guard: any, router: Router); - static forChild(routes: Routes): ModuleWithProviders; - static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders; + static forChild(routes: Routes): ModuleWithProviders; + static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders; } export declare class RouterOutlet implements OnDestroy, OnInit { diff --git a/tools/public_api_guard/router/testing.d.ts b/tools/public_api_guard/router/testing.d.ts index d401d5a72d..a6396352c0 100644 --- a/tools/public_api_guard/router/testing.d.ts +++ b/tools/public_api_guard/router/testing.d.ts @@ -1,5 +1,5 @@ export declare class RouterTestingModule { - static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders; + static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders; } export declare function setupTestingRouter(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions, urlHandlingStrategy?: UrlHandlingStrategy): Router;