feat(ivy): compile queries in ngtsc (#24862)
This commit adds support for @ContentChild[ren] and @ViewChild[ren] in ngtsc. Previously queries were ignored. PR Close #24862
This commit is contained in:
		
							parent
							
								
									6eb6ac7c12
								
							
						
					
					
						commit
						76f8f78920
					
				| @ -11,11 +11,11 @@ import * as path from 'path'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {Decorator, ReflectionHost} from '../../host'; | ||||
| import {reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; | ||||
| 
 | ||||
| import {ResourceLoader} from './api'; | ||||
| import {extractDirectiveMetadata} from './directive'; | ||||
| import {extractDirectiveMetadata, extractQueriesFromDecorator, queriesFromFields} from './directive'; | ||||
| import {SelectorScopeRegistry} from './selector_scope'; | ||||
| import {isAngularCore, unwrapExpression} from './util'; | ||||
| 
 | ||||
| @ -59,9 +59,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe | ||||
| 
 | ||||
|     // @Component inherits @Directive, so begin by extracting the @Directive metadata and building
 | ||||
|     // on it.
 | ||||
|     const directiveMetadata = | ||||
|     const directiveResult = | ||||
|         extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore); | ||||
|     if (directiveMetadata === undefined) { | ||||
|     if (directiveResult === undefined) { | ||||
|       // `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
 | ||||
|       // case, compilation of the decorator is skipped. Returning an empty object signifies
 | ||||
|       // that no analysis was produced.
 | ||||
| @ -69,7 +69,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe | ||||
|     } | ||||
| 
 | ||||
|     // Next, read the `@Component`-specific fields.
 | ||||
|     const component = reflectObjectLiteral(meta); | ||||
|     const {decoratedElements, decorator: component, metadata} = directiveResult; | ||||
| 
 | ||||
|     let templateStr: string|null = null; | ||||
|     if (component.has('templateUrl')) { | ||||
| @ -109,15 +109,29 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe | ||||
| 
 | ||||
|     // If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
 | ||||
|     // when this component appears in an `@NgModule` scope, its selector can be determined.
 | ||||
|     if (directiveMetadata.selector !== null) { | ||||
|       this.scopeRegistry.registerSelector(node, directiveMetadata.selector); | ||||
|     if (metadata.selector !== null) { | ||||
|       this.scopeRegistry.registerSelector(node, metadata.selector); | ||||
|     } | ||||
| 
 | ||||
|     // Construct the list of view queries.
 | ||||
|     const coreModule = this.isCore ? undefined : '@angular/core'; | ||||
|     const viewChildFromFields = queriesFromFields( | ||||
|         filterToMembersWithDecorator(decoratedElements, 'ViewChild', coreModule), this.checker); | ||||
|     const viewChildrenFromFields = queriesFromFields( | ||||
|         filterToMembersWithDecorator(decoratedElements, 'ViewChildren', coreModule), this.checker); | ||||
|     const viewQueries = [...viewChildFromFields, ...viewChildrenFromFields]; | ||||
| 
 | ||||
|     if (component.has('queries')) { | ||||
|       const queriesFromDecorator = extractQueriesFromDecorator( | ||||
|           component.get('queries') !, this.reflector, this.checker, this.isCore); | ||||
|       viewQueries.push(...queriesFromDecorator.view); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       analysis: { | ||||
|         ...directiveMetadata, | ||||
|         ...metadata, | ||||
|         template, | ||||
|         viewQueries: [], | ||||
|         viewQueries, | ||||
| 
 | ||||
|         // These will be replaced during the compilation step, after all `NgModule`s have been
 | ||||
|         // analyzed and the full compilation scope for the component can be realized.
 | ||||
|  | ||||
| @ -6,11 +6,11 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler'; | ||||
| import {ConstantPool, Expression, R3DirectiveMetadata, R3QueryMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler'; | ||||
| import * as ts from 'typescript'; | ||||
| 
 | ||||
| import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host'; | ||||
| import {filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {Reference, filterToMembersWithDecorator, reflectObjectLiteral, staticallyResolve} from '../../metadata'; | ||||
| import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; | ||||
| 
 | ||||
| import {SelectorScopeRegistry} from './selector_scope'; | ||||
| @ -29,8 +29,9 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe | ||||
|   } | ||||
| 
 | ||||
|   analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> { | ||||
|     const analysis = | ||||
|     const directiveResult = | ||||
|         extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore); | ||||
|     const analysis = directiveResult && directiveResult.metadata; | ||||
| 
 | ||||
|     // If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
 | ||||
|     // when this directive appears in an `@NgModule` scope, its selector can be determined.
 | ||||
| @ -58,7 +59,11 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe | ||||
|  */ | ||||
| export function extractDirectiveMetadata( | ||||
|     clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker, | ||||
|     reflector: ReflectionHost, isCore: boolean): R3DirectiveMetadata|undefined { | ||||
|     reflector: ReflectionHost, isCore: boolean): { | ||||
|   decorator: Map<string, ts.Expression>, | ||||
|   metadata: R3DirectiveMetadata, | ||||
|   decoratedElements: ClassMember[], | ||||
| }|undefined { | ||||
|   if (decorator.args === null || decorator.args.length !== 1) { | ||||
|     throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`); | ||||
|   } | ||||
| @ -93,6 +98,19 @@ export function extractDirectiveMetadata( | ||||
|   const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker); | ||||
|   const outputsFromFields = parseDecoratedFields( | ||||
|       filterToMembersWithDecorator(decoratedElements, 'Output', coreModule), checker); | ||||
|   // Construct the list of queries.
 | ||||
|   const contentChildFromFields = queriesFromFields( | ||||
|       filterToMembersWithDecorator(decoratedElements, 'ContentChild', coreModule), checker); | ||||
|   const contentChildrenFromFields = queriesFromFields( | ||||
|       filterToMembersWithDecorator(decoratedElements, 'ContentChildren', coreModule), checker); | ||||
| 
 | ||||
|   const queries = [...contentChildFromFields, ...contentChildrenFromFields]; | ||||
| 
 | ||||
|   if (directive.has('queries')) { | ||||
|     const queriesFromDecorator = | ||||
|         extractQueriesFromDecorator(directive.get('queries') !, reflector, checker, isCore); | ||||
|     queries.push(...queriesFromDecorator.content); | ||||
|   } | ||||
| 
 | ||||
|   // Parse the selector.
 | ||||
|   let selector = ''; | ||||
| @ -112,7 +130,7 @@ export function extractDirectiveMetadata( | ||||
|   // Detect if the component inherits from another class
 | ||||
|   const usesInheritance = clazz.heritageClauses !== undefined && | ||||
|       clazz.heritageClauses.some(hc => hc.token === ts.SyntaxKind.ExtendsKeyword); | ||||
|   return { | ||||
|   const metadata: R3DirectiveMetadata = { | ||||
|     name: clazz.name !.text, | ||||
|     deps: getConstructorDependencies(clazz, reflector, isCore), | ||||
|     host: { | ||||
| @ -124,17 +142,105 @@ export function extractDirectiveMetadata( | ||||
|         usesOnChanges, | ||||
|     }, | ||||
|     inputs: {...inputsFromMeta, ...inputsFromFields}, | ||||
|     outputs: {...outputsFromMeta, ...outputsFromFields}, | ||||
|     queries: [], selector, | ||||
|     outputs: {...outputsFromMeta, ...outputsFromFields}, queries, selector, | ||||
|     type: new WrappedNodeExpr(clazz.name !), | ||||
|     typeSourceSpan: null !, usesInheritance, | ||||
|   }; | ||||
|   return {decoratedElements, decorator: directive, metadata}; | ||||
| } | ||||
| 
 | ||||
| function assertIsStringArray(value: any[]): value is string[] { | ||||
| export function extractQueryMetadata( | ||||
|     name: string, args: ReadonlyArray<ts.Expression>, propertyName: string, | ||||
|     checker: ts.TypeChecker): R3QueryMetadata { | ||||
|   if (args.length === 0) { | ||||
|     throw new Error(`@${name} must have arguments`); | ||||
|   } | ||||
|   const first = name === 'ViewChild' || name === 'ContentChild'; | ||||
|   const arg = staticallyResolve(args[0], checker); | ||||
| 
 | ||||
|   // Extract the predicate
 | ||||
|   let predicate: Expression|string[]|null = null; | ||||
|   if (arg instanceof Reference) { | ||||
|     predicate = new WrappedNodeExpr(args[0]); | ||||
|   } else if (typeof arg === 'string') { | ||||
|     predicate = [arg]; | ||||
|   } else if (isStringArrayOrDie(arg, '@' + name)) { | ||||
|     predicate = arg as string[]; | ||||
|   } else { | ||||
|     throw new Error(`@${name} predicate cannot be interpreted`); | ||||
|   } | ||||
| 
 | ||||
|   // Extract the read and descendants options.
 | ||||
|   let read: Expression|null = null; | ||||
|   // The default value for descendants is true for every decorator except @ContentChildren.
 | ||||
|   let descendants: boolean = name !== 'ContentChildren'; | ||||
|   if (args.length === 2) { | ||||
|     const optionsExpr = unwrapExpression(args[1]); | ||||
|     if (!ts.isObjectLiteralExpression(optionsExpr)) { | ||||
|       throw new Error(`@${name} options must be an object literal`); | ||||
|     } | ||||
|     const options = reflectObjectLiteral(optionsExpr); | ||||
|     if (options.has('read')) { | ||||
|       read = new WrappedNodeExpr(options.get('read') !); | ||||
|     } | ||||
| 
 | ||||
|     if (options.has('descendants')) { | ||||
|       const descendantsValue = staticallyResolve(options.get('descendants') !, checker); | ||||
|       if (typeof descendantsValue !== 'boolean') { | ||||
|         throw new Error(`@${name} options.descendants must be a boolean`); | ||||
|       } | ||||
|       descendants = descendantsValue; | ||||
|     } | ||||
|   } else if (args.length > 2) { | ||||
|     // Too many arguments.
 | ||||
|     throw new Error(`@${name} has too many arguments`); | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|       propertyName, predicate, first, descendants, read, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function extractQueriesFromDecorator( | ||||
|     queryData: ts.Expression, reflector: ReflectionHost, checker: ts.TypeChecker, | ||||
|     isCore: boolean): { | ||||
|   content: R3QueryMetadata[], | ||||
|   view: R3QueryMetadata[], | ||||
| } { | ||||
|   const content: R3QueryMetadata[] = [], view: R3QueryMetadata[] = []; | ||||
|   const expr = unwrapExpression(queryData); | ||||
|   if (!ts.isObjectLiteralExpression(queryData)) { | ||||
|     throw new Error(`queries metadata must be an object literal`); | ||||
|   } | ||||
|   reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => { | ||||
|     queryExpr = unwrapExpression(queryExpr); | ||||
|     if (!ts.isNewExpression(queryExpr) || !ts.isIdentifier(queryExpr.expression)) { | ||||
|       throw new Error(`query metadata must be an instance of a query type`); | ||||
|     } | ||||
|     const type = reflector.getImportOfIdentifier(queryExpr.expression); | ||||
|     if (type === null || (!isCore && type.from !== '@angular/core') || | ||||
|         !QUERY_TYPES.has(type.name)) { | ||||
|       throw new Error(`query metadata must be an instance of a query type`); | ||||
|     } | ||||
| 
 | ||||
|     const query = extractQueryMetadata(type.name, queryExpr.arguments || [], propertyName, checker); | ||||
|     if (type.name.startsWith('Content')) { | ||||
|       content.push(query); | ||||
|     } else { | ||||
|       view.push(query); | ||||
|     } | ||||
|   }); | ||||
|   return {content, view}; | ||||
| } | ||||
| 
 | ||||
| function isStringArrayOrDie(value: any, name: string): value is string[] { | ||||
|   if (!Array.isArray(value)) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   for (let i = 0; i < value.length; i++) { | ||||
|     if (typeof value[i] !== 'string') { | ||||
|       throw new Error(`Failed to resolve @Directive.inputs[${i}] to a string`); | ||||
|       throw new Error(`Failed to resolve ${name}[${i}] to a string`); | ||||
|     } | ||||
|   } | ||||
|   return true; | ||||
| @ -153,7 +259,7 @@ function parseFieldToPropertyMapping( | ||||
| 
 | ||||
|   // Resolve the field of interest from the directive metadata to a string[].
 | ||||
|   const metaValues = staticallyResolve(directive.get(field) !, checker); | ||||
|   if (!Array.isArray(metaValues) || !assertIsStringArray(metaValues)) { | ||||
|   if (!isStringArrayOrDie(metaValues, field)) { | ||||
|     throw new Error(`Failed to resolve @Directive.${field}`); | ||||
|   } | ||||
| 
 | ||||
| @ -199,3 +305,29 @@ function parseDecoratedFields( | ||||
|       }, | ||||
|       {} as{[field: string]: string}); | ||||
| } | ||||
| 
 | ||||
| export function queriesFromFields( | ||||
|     fields: {member: ClassMember, decorators: Decorator[]}[], | ||||
|     checker: ts.TypeChecker): R3QueryMetadata[] { | ||||
|   return fields.map(({member, decorators}) => { | ||||
|     if (decorators.length !== 1) { | ||||
|       throw new Error(`Cannot have multiple query decorators on the same class member`); | ||||
|     } else if (!isPropertyTypeMember(member)) { | ||||
|       throw new Error(`Query decorator must go on a property-type member`); | ||||
|     } | ||||
|     const decorator = decorators[0]; | ||||
|     return extractQueryMetadata(decorator.name, decorator.args || [], member.name, checker); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function isPropertyTypeMember(member: ClassMember): boolean { | ||||
|   return member.kind === ClassMemberKind.Getter || member.kind === ClassMemberKind.Setter || | ||||
|       member.kind === ClassMemberKind.Property; | ||||
| } | ||||
| 
 | ||||
| const QUERY_TYPES = new Set([ | ||||
|   'ContentChild', | ||||
|   'ContentChildren', | ||||
|   'ViewChild', | ||||
|   'ViewChildren', | ||||
| ]); | ||||
|  | ||||
| @ -6,7 +6,10 @@ | ||||
|  * found in the LICENSE file at https://angular.io/license
 | ||||
|  */ | ||||
| 
 | ||||
| type FnWithArg<T> = (arg?: any) => T; | ||||
| interface FnWithArg<T> { | ||||
|   (...args: any[]): T; | ||||
|   new (...args: any[]): T; | ||||
| } | ||||
| 
 | ||||
| function callableClassDecorator(): FnWithArg<(clazz: any) => any> { | ||||
|   return null !; | ||||
| @ -16,6 +19,10 @@ function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> { | ||||
|   return null !; | ||||
| } | ||||
| 
 | ||||
| function callablePropDecorator(): FnWithArg<(a: any, b: any) => any> { | ||||
|   return null !; | ||||
| } | ||||
| 
 | ||||
| export const Component = callableClassDecorator(); | ||||
| export const Directive = callableClassDecorator(); | ||||
| export const Injectable = callableClassDecorator(); | ||||
| @ -27,6 +34,12 @@ export const Inject = callableParamDecorator(); | ||||
| export const Self = callableParamDecorator(); | ||||
| export const SkipSelf = callableParamDecorator(); | ||||
| export const Optional = callableParamDecorator(); | ||||
| 
 | ||||
| export const ContentChild = callablePropDecorator(); | ||||
| export const ContentChildren = callablePropDecorator(); | ||||
| export const ViewChild = callablePropDecorator(); | ||||
| export const ViewChildren = callablePropDecorator(); | ||||
| 
 | ||||
| export type ModuleWithProviders<T> = any; | ||||
| 
 | ||||
| export class ChangeDetectorRef {} | ||||
|  | ||||
| @ -412,4 +412,36 @@ describe('ngtsc behavioral tests', () => { | ||||
|         .toContain( | ||||
|             `factory: function FooCmp_Factory() { return new FooCmp(i0.ɵinjectAttribute("test"), i0.ɵinjectChangeDetectorRef(), i0.ɵinjectElementRef(), i0.ɵdirectiveInject(i0.INJECTOR), i0.ɵinjectTemplateRef(), i0.ɵinjectViewContainerRef()); }`); | ||||
|   }); | ||||
| 
 | ||||
|   it('should generate queries for components', () => { | ||||
|     writeConfig(); | ||||
|     write(`test.ts`, ` | ||||
|         import {Component, ContentChild, ContentChildren, TemplateRef, ViewChild} from '@angular/core'; | ||||
| 
 | ||||
|         @Component({ | ||||
|           selector: 'test', | ||||
|           template: '<div #foo></div>', | ||||
|           queries: { | ||||
|             'mview': new ViewChild('test1'), | ||||
|             'mcontent': new ContentChild('test2'), | ||||
|           } | ||||
|         }) | ||||
|         class FooCmp { | ||||
|           @ContentChild('bar', {read: TemplateRef}) child: any; | ||||
|           @ContentChildren(TemplateRef) children: any; | ||||
|           get aview(): any { return null; } | ||||
|           @ViewChild('accessor') set aview(value: any) {} | ||||
|         } | ||||
|     `);
 | ||||
| 
 | ||||
|     const exitCode = main(['-p', basePath], errorSpy); | ||||
|     expect(errorSpy).not.toHaveBeenCalled(); | ||||
|     expect(exitCode).toBe(0); | ||||
|     const jsContents = getContents('test.js'); | ||||
|     expect(jsContents).toContain(`i0.ɵQ(null, ["bar"], true, TemplateRef)`); | ||||
|     expect(jsContents).toContain(`i0.ɵQ(null, TemplateRef, false)`); | ||||
|     expect(jsContents).toContain(`i0.ɵQ(null, ["test2"], true)`); | ||||
|     expect(jsContents).toContain(`i0.ɵQ(0, ["accessor"], true)`); | ||||
|     expect(jsContents).toContain(`i0.ɵQ(1, ["test1"], true)`); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user