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:
Alex Rickabaugh 2018-07-18 09:32:36 -07:00 committed by Victor Berchet
parent 6eb6ac7c12
commit 76f8f78920
4 changed files with 211 additions and 20 deletions

View File

@ -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.

View File

@ -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',
]);

View File

@ -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 {}

View File

@ -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)`);
});
});