feat(ivy): selector side of ModuleWithProviders via type metadata (#24862)
Within an @NgModule it's common to include in the imports a call to a ModuleWithProviders function, for example RouterModule.forRoot(). The old ngc compiler was able to handle this pattern because it had global knowledge of metadata of not only the input compilation unit but also all dependencies. The ngtsc compiler for Ivy doesn't have this knowledge, so the pattern of ModuleWithProviders functions is more difficult. ngtsc must be able to determine which module is imported via the function in order to expand the selector scope and properly tree-shake directives and pipes. This commit implements a solution to this problem, by adding a type parameter to ModuleWithProviders through which the actual module type can be passed between compilation units. The provider side isn't a problem because the imports are always copied directly to the ngInjectorDef. PR Close #24862
This commit is contained in:
parent
1008bb6287
commit
60aeee7abf
|
@ -64,12 +64,16 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||||
}
|
}
|
||||||
let imports: Reference[] = [];
|
let imports: Reference[] = [];
|
||||||
if (ngModule.has('imports')) {
|
if (ngModule.has('imports')) {
|
||||||
const importsMeta = staticallyResolve(ngModule.get('imports') !, this.checker);
|
const importsMeta = staticallyResolve(
|
||||||
|
ngModule.get('imports') !, this.checker,
|
||||||
|
node => this._extractModuleFromModuleWithProvidersFn(node));
|
||||||
imports = resolveTypeList(importsMeta, 'imports');
|
imports = resolveTypeList(importsMeta, 'imports');
|
||||||
}
|
}
|
||||||
let exports: Reference[] = [];
|
let exports: Reference[] = [];
|
||||||
if (ngModule.has('exports')) {
|
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');
|
exports = resolveTypeList(exportsMeta, 'exports');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,6 +129,46 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a `FunctionDeclaration` or `MethodDeclaration`, check if it is typed as a
|
||||||
|
* `ModuleWithProviders` and return an expression referencing the module if available.
|
||||||
|
*/
|
||||||
|
private _extractModuleFromModuleWithProvidersFn(node: ts.FunctionDeclaration|
|
||||||
|
ts.MethodDeclaration): ts.Expression|null {
|
||||||
|
const type = node.type;
|
||||||
|
// Examine the type of the function to see if it's a ModuleWithProviders reference.
|
||||||
|
if (type === undefined || !ts.isTypeReferenceNode(type) || !ts.isIdentifier(type.typeName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look at the type itself to see where it comes from.
|
||||||
|
const id = this.reflector.getImportOfIdentifier(type.typeName);
|
||||||
|
|
||||||
|
// If it's not named ModuleWithProviders, bail.
|
||||||
|
if (id === null || id.name !== 'ModuleWithProviders') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not from @angular/core, bail.
|
||||||
|
if (!this.isCore && id.from !== '@angular/core') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's no type parameter specified, bail.
|
||||||
|
if (type.typeArguments === undefined || type.typeArguments.length !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const arg = type.typeArguments[0];
|
||||||
|
|
||||||
|
// If the argument isn't an Identifier, bail.
|
||||||
|
if (!ts.isTypeReferenceNode(arg) || !ts.isIdentifier(arg.typeName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg.typeName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -107,6 +107,8 @@ export abstract class Reference {
|
||||||
* referenceable.
|
* referenceable.
|
||||||
*/
|
*/
|
||||||
export class NodeReference extends Reference {
|
export class NodeReference extends Reference {
|
||||||
|
constructor(node: ts.Node, readonly moduleName: string|null) { super(node); }
|
||||||
|
|
||||||
toExpression(context: ts.SourceFile): null { return null; }
|
toExpression(context: ts.SourceFile): null { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,11 +179,22 @@ export class AbsoluteReference extends Reference {
|
||||||
*
|
*
|
||||||
* @param node the expression to statically resolve if possible
|
* @param node the expression to statically resolve if possible
|
||||||
* @param checker a `ts.TypeChecker` used to understand the expression
|
* @param checker a `ts.TypeChecker` used to understand the expression
|
||||||
|
* @param foreignFunctionResolver a function which will be used whenever a "foreign function" is
|
||||||
|
* encountered. A foreign function is a function which has no body - usually the result of calling
|
||||||
|
* a function declared in another library's .d.ts file. In these cases, the foreignFunctionResolver
|
||||||
|
* will be called with the function's declaration, and can optionally return a `ts.Expression`
|
||||||
|
* (possibly extracted from the foreign function's type signature) which will be used as the result
|
||||||
|
* of the call.
|
||||||
* @returns a `ResolvedValue` representing the resolved value
|
* @returns a `ResolvedValue` representing the resolved value
|
||||||
*/
|
*/
|
||||||
export function staticallyResolve(node: ts.Expression, checker: ts.TypeChecker): ResolvedValue {
|
export function staticallyResolve(
|
||||||
return new StaticInterpreter(checker).visit(
|
node: ts.Expression, checker: ts.TypeChecker,
|
||||||
node, {absoluteModuleName: null, scope: new Map<ts.ParameterDeclaration, ResolvedValue>()});
|
foreignFunctionResolver?: (node: ts.FunctionDeclaration | ts.MethodDeclaration) =>
|
||||||
|
ts.Expression | null): ResolvedValue {
|
||||||
|
return new StaticInterpreter(checker).visit(node, {
|
||||||
|
absoluteModuleName: null,
|
||||||
|
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BinaryOperatorDef {
|
interface BinaryOperatorDef {
|
||||||
|
@ -226,6 +239,7 @@ const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
|
||||||
interface Context {
|
interface Context {
|
||||||
absoluteModuleName: string|null;
|
absoluteModuleName: string|null;
|
||||||
scope: Scope;
|
scope: Scope;
|
||||||
|
foreignFunctionResolver?(node: ts.FunctionDeclaration|ts.MethodDeclaration): ts.Expression|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class StaticInterpreter {
|
class StaticInterpreter {
|
||||||
|
@ -472,6 +486,10 @@ class StaticInterpreter {
|
||||||
} else if (lhs instanceof Reference) {
|
} else if (lhs instanceof Reference) {
|
||||||
const ref = lhs.node;
|
const ref = lhs.node;
|
||||||
if (ts.isClassDeclaration(ref)) {
|
if (ts.isClassDeclaration(ref)) {
|
||||||
|
let absoluteModuleName = context.absoluteModuleName;
|
||||||
|
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
|
||||||
|
absoluteModuleName = lhs.moduleName || absoluteModuleName;
|
||||||
|
}
|
||||||
let value: ResolvedValue = undefined;
|
let value: ResolvedValue = undefined;
|
||||||
const member =
|
const member =
|
||||||
ref.members.filter(member => isStatic(member))
|
ref.members.filter(member => isStatic(member))
|
||||||
|
@ -482,7 +500,7 @@ class StaticInterpreter {
|
||||||
if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) {
|
if (ts.isPropertyDeclaration(member) && member.initializer !== undefined) {
|
||||||
value = this.visitExpression(member.initializer, context);
|
value = this.visitExpression(member.initializer, context);
|
||||||
} else if (ts.isMethodDeclaration(member)) {
|
} else if (ts.isMethodDeclaration(member)) {
|
||||||
value = new NodeReference(member);
|
value = new NodeReference(member, absoluteModuleName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
@ -495,13 +513,35 @@ class StaticInterpreter {
|
||||||
const lhs = this.visitExpression(node.expression, context);
|
const lhs = this.visitExpression(node.expression, context);
|
||||||
if (!(lhs instanceof Reference)) {
|
if (!(lhs instanceof Reference)) {
|
||||||
throw new Error(`attempting to call something that is not a function: ${lhs}`);
|
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(
|
throw new Error(
|
||||||
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]}`);
|
`calling something that is not a function declaration? ${ts.SyntaxKind[lhs.node.kind]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn = lhs.node;
|
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])) {
|
if (body.statements.length !== 1 || !ts.isReturnStatement(body.statements[0])) {
|
||||||
throw new Error('Function body must have a single return statement only.');
|
throw new Error('Function body must have a single return statement only.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,3 +26,4 @@ export const Inject = callableParamDecorator();
|
||||||
export const Self = callableParamDecorator();
|
export const Self = callableParamDecorator();
|
||||||
export const SkipSelf = callableParamDecorator();
|
export const SkipSelf = callableParamDecorator();
|
||||||
export const Optional = callableParamDecorator();
|
export const Optional = callableParamDecorator();
|
||||||
|
export type ModuleWithProviders<T> = any;
|
||||||
|
|
|
@ -344,4 +344,30 @@ describe('ngtsc behavioral tests', () => {
|
||||||
const dtsContents = getContents('test.d.ts');
|
const dtsContents = getContents('test.d.ts');
|
||||||
expect(dtsContents).toContain('i0.ɵNgModuleDef<TestModule, [TestPipe,TestCmp], [], []>');
|
expect(dtsContents).toContain('i0.ɵNgModuleDef<TestModule, [TestPipe,TestCmp], [], []>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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<RouterModule>;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
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<TestModule, [], [i1.RouterModule], []>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -73,10 +73,11 @@ export interface NgModuleDef<T, Declarations, Imports, Exports> {
|
||||||
/**
|
/**
|
||||||
* A wrapper around an NgModule that associates it with the providers.
|
* 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 {
|
export interface ModuleWithProviders<T = any> {
|
||||||
ngModule: Type<any>;
|
ngModule: Type<T>;
|
||||||
providers?: Provider[];
|
providers?: Provider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export class FormsModule {
|
||||||
export class ReactiveFormsModule {
|
export class ReactiveFormsModule {
|
||||||
static withConfig(opts: {
|
static withConfig(opts: {
|
||||||
/** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never' | 'once' | 'always'
|
/** @deprecated as of v6 */ warnOnNgModelWithFormControl: 'never' | 'once' | 'always'
|
||||||
}): ModuleWithProviders {
|
}): ModuleWithProviders<ReactiveFormsModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: ReactiveFormsModule,
|
ngModule: ReactiveFormsModule,
|
||||||
providers: [{
|
providers: [{
|
||||||
|
|
|
@ -112,7 +112,7 @@ export class BrowserModule {
|
||||||
*
|
*
|
||||||
* @experimental
|
* @experimental
|
||||||
*/
|
*/
|
||||||
static withServerTransition(params: {appId: string}): ModuleWithProviders {
|
static withServerTransition(params: {appId: string}): ModuleWithProviders<BrowserModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: BrowserModule,
|
ngModule: BrowserModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -155,7 +155,7 @@ export class RouterModule {
|
||||||
* * `paramsInheritanceStrategy` defines how the router merges params, data and resolved data
|
* * `paramsInheritanceStrategy` defines how the router merges params, data and resolved data
|
||||||
* from parent to child routes.
|
* from parent to child routes.
|
||||||
*/
|
*/
|
||||||
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders {
|
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: RouterModule,
|
ngModule: RouterModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
@ -193,7 +193,7 @@ export class RouterModule {
|
||||||
/**
|
/**
|
||||||
* Creates a module with all the router directives and a provider registering routes.
|
* Creates a module with all the router directives and a provider registering routes.
|
||||||
*/
|
*/
|
||||||
static forChild(routes: Routes): ModuleWithProviders {
|
static forChild(routes: Routes): ModuleWithProviders<RouterModule> {
|
||||||
return {ngModule: RouterModule, providers: [provideRoutes(routes)]};
|
return {ngModule: RouterModule, providers: [provideRoutes(routes)]};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,7 +180,8 @@ export function setupTestingRouter(
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class RouterTestingModule {
|
export class RouterTestingModule {
|
||||||
static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders {
|
static withRoutes(routes: Routes, config?: ExtraOptions):
|
||||||
|
ModuleWithProviders<RouterTestingModule> {
|
||||||
return {
|
return {
|
||||||
ngModule: RouterTestingModule,
|
ngModule: RouterTestingModule,
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
@ -510,8 +510,8 @@ export declare class ModuleWithComponentFactories<T> {
|
||||||
constructor(ngModuleFactory: NgModuleFactory<T>, componentFactories: ComponentFactory<any>[]);
|
constructor(ngModuleFactory: NgModuleFactory<T>, componentFactories: ComponentFactory<any>[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModuleWithProviders {
|
export interface ModuleWithProviders<T = any> {
|
||||||
ngModule: Type<any>;
|
ngModule: Type<T>;
|
||||||
providers?: Provider[];
|
providers?: Provider[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -458,7 +458,7 @@ export declare class RadioControlValueAccessor implements ControlValueAccessor,
|
||||||
|
|
||||||
export declare class ReactiveFormsModule {
|
export declare class ReactiveFormsModule {
|
||||||
static withConfig(opts: { warnOnNgModelWithFormControl: 'never' | 'once' | 'always';
|
static withConfig(opts: { warnOnNgModelWithFormControl: 'never' | 'once' | 'always';
|
||||||
}): ModuleWithProviders;
|
}): ModuleWithProviders<ReactiveFormsModule>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class RequiredValidator implements Validator {
|
export declare class RequiredValidator implements Validator {
|
||||||
|
|
|
@ -2,7 +2,7 @@ export declare class BrowserModule {
|
||||||
constructor(parentModule: BrowserModule | null);
|
constructor(parentModule: BrowserModule | null);
|
||||||
/** @experimental */ static withServerTransition(params: {
|
/** @experimental */ static withServerTransition(params: {
|
||||||
appId: string;
|
appId: string;
|
||||||
}): ModuleWithProviders;
|
}): ModuleWithProviders<BrowserModule>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @experimental */
|
/** @experimental */
|
||||||
|
|
|
@ -414,8 +414,8 @@ export declare class RouterLinkWithHref implements OnChanges, OnDestroy {
|
||||||
|
|
||||||
export declare class RouterModule {
|
export declare class RouterModule {
|
||||||
constructor(guard: any, router: Router);
|
constructor(guard: any, router: Router);
|
||||||
static forChild(routes: Routes): ModuleWithProviders;
|
static forChild(routes: Routes): ModuleWithProviders<RouterModule>;
|
||||||
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders;
|
static forRoot(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterModule>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare class RouterOutlet implements OnDestroy, OnInit {
|
export declare class RouterOutlet implements OnDestroy, OnInit {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export declare class RouterTestingModule {
|
export declare class RouterTestingModule {
|
||||||
static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders;
|
static withRoutes(routes: Routes, config?: ExtraOptions): ModuleWithProviders<RouterTestingModule>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export declare function setupTestingRouter(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions, urlHandlingStrategy?: UrlHandlingStrategy): Router;
|
export declare function setupTestingRouter(urlSerializer: UrlSerializer, contexts: ChildrenOutletContexts, location: Location, loader: NgModuleFactoryLoader, compiler: Compiler, injector: Injector, routes: Route[][], opts?: ExtraOptions, urlHandlingStrategy?: UrlHandlingStrategy): Router;
|
||||||
|
|
Loading…
Reference in New Issue