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};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function assertIsStringArray(value: any[]): value is string[] {
|
||||
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…
Reference in New Issue