feat(compiler): add dependency info and ng-content selectors to metadata (#35695)

This commit augments the `FactoryDef` declaration of Angular decorated
classes to contain information about the parameter decorators used in
the constructor. If no constructor is present, or none of the parameters
have any Angular decorators, then this will be represented using the
`null` type. Otherwise, a tuple type is used where the entry at index `i`
corresponds with parameter `i`. Each tuple entry can be one of two types:

1. If the associated parameter does not have any Angular decorators,
   the tuple entry will be the `null` type.
2. Otherwise, a type literal is used that may declare at least one of
   the following properties:
   - "attribute": if `@Attribute` is present. The injected attribute's
   name is used as string literal type, or the `unknown` type if the
   attribute name is not a string literal.
   - "self": if `@Self` is present, always of type `true`.
   - "skipSelf": if `@SkipSelf` is present, always of type `true`.
   - "host": if `@Host` is present, always of type `true`.
   - "optional": if `@Optional` is present, always of type `true`.

   A property is only present if the corresponding decorator is used.

   Note that the `@Inject` decorator is currently not included, as it's
   non-trivial to properly convert the token's value expression to a
   type that is valid in a declaration file.

Additionally, the `ComponentDefWithMeta` declaration that is created for
Angular components has been extended to include all selectors on
`ng-content` elements within the component's template.

This additional metadata is useful for tooling such as the Angular
Language Service, as it provides the ability to offer suggestions for
directives/components defined in libraries. At the moment, such
tooling extracts the necessary information from the _metadata.json_
manifest file as generated by ngc, however this metadata representation
is being replaced by the information emitted into the declaration files.

Resolves FW-1870

PR Close #35695
This commit is contained in:
JoostK
2020-02-26 22:05:44 +01:00
committed by Misko Hevery
parent ff4eb0cb63
commit 32ce8b1326
19 changed files with 371 additions and 67 deletions

View File

@ -313,6 +313,7 @@ export class ComponentDecoratorHandler implements
...metadata,
template: {
nodes: template.emitNodes,
ngContentSelectors: template.ngContentSelectors,
},
encapsulation,
interpolation: template.interpolation,
@ -770,12 +771,13 @@ export class ComponentDecoratorHandler implements
interpolation = InterpolationConfig.fromArray(value as[string, string]);
}
const {errors, nodes: emitNodes, styleUrls, styles} = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
});
const {errors, nodes: emitNodes, styleUrls, styles, ngContentSelectors} =
parseTemplate(templateStr, templateUrl, {
preserveWhitespaces,
interpolationConfig: interpolation,
range: templateRange, escapedString,
enableI18nLegacyMessageIdFormat: this.enableI18nLegacyMessageIdFormat,
});
// Unfortunately, the primary parse of the template above may not contain accurate source map
// information. If used directly, it would result in incorrect code locations in template
@ -804,6 +806,7 @@ export class ComponentDecoratorHandler implements
diagNodes,
styleUrls,
styles,
ngContentSelectors,
errors,
template: templateStr, templateUrl,
isInline: component.has('template'),
@ -923,6 +926,11 @@ export interface ParsedTemplate {
*/
styles: string[];
/**
* Any ng-content selectors extracted from the template.
*/
ngContentSelectors: string[];
/**
* Whether the template was inline.
*/

View File

@ -274,6 +274,7 @@ function extractInjectableCtorDeps(
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
const meta: R3DependencyMetadata = {
token: new WrappedNodeExpr(dep),
attribute: null,
host: false,
resolved: R3ResolvedDependencyType.Token,
optional: false,

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Expression, ExternalExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, LiteralExpr, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
@ -48,6 +48,7 @@ export function getConstructorDependencies(
}
ctorParams.forEach((param, idx) => {
let token = valueReferenceToExpression(param.typeValueReference, defaultImportRecorder);
let attribute: Expression|null = null;
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
@ -74,7 +75,13 @@ export function getConstructorDependencies(
ErrorCode.DECORATOR_ARITY_WRONG, Decorator.nodeForError(dec),
`Unexpected number of arguments to @Attribute().`);
}
token = new WrappedNodeExpr(dec.args[0]);
const attributeName = dec.args[0];
token = new WrappedNodeExpr(attributeName);
if (ts.isStringLiteralLike(attributeName)) {
attribute = new LiteralExpr(attributeName.text);
} else {
attribute = new WrappedNodeExpr(ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
}
resolved = R3ResolvedDependencyType.Attribute;
} else {
throw new FatalDiagnosticError(
@ -93,7 +100,7 @@ export function getConstructorDependencies(
kind: ConstructorDepErrorKind.NO_SUITABLE_TOKEN, param,
});
} else {
deps.push({token, optional, self, skipSelf, host, resolved});
deps.push({token, attribute, optional, self, skipSelf, host, resolved});
}
});
if (errors.length === 0) {
@ -369,7 +376,8 @@ const parensWrapperTransformerFactory: ts.TransformerFactory<ts.Expression> =
/**
* Wraps all functions in a given expression in parentheses. This is needed to avoid problems
* where Tsickle annotations added between analyse and transform phases in Angular may trigger
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement. More
* automatic semicolon insertion, e.g. if a function is the expression in a `return` statement.
* More
* info can be found in Tsickle source code here:
* https://github.com/angular/tsickle/blob/d7974262571c8a17d684e5ba07680e1b1993afdd/src/jsdoc_transformer.ts#L1021
*

View File

@ -205,7 +205,7 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
const newMembers = fields.map(decl => {
const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)];
const typeRef = translateType(decl.type, imports);
emitAsSingleLine(typeRef);
markForEmitAsSingleLine(typeRef);
return ts.createProperty(
/* decorators */ undefined,
/* modifiers */ modifiers,
@ -226,9 +226,9 @@ export class IvyDeclarationDtsTransform implements DtsTransform {
}
}
function emitAsSingleLine(node: ts.Node) {
function markForEmitAsSingleLine(node: ts.Node) {
ts.setEmitFlags(node, ts.EmitFlags.SingleLine);
ts.forEachChild(node, emitAsSingleLine);
ts.forEachChild(node, markForEmitAsSingleLine);
}
export class ReturnTypeTransform implements DtsTransform {

View File

@ -447,12 +447,12 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
`An ExpressionType with type arguments cannot have multiple levels of type arguments`);
}
const typeArgs = type.typeParams.map(param => param.visitType(this, context));
const typeArgs = type.typeParams.map(param => this.translateType(param, context));
return ts.createTypeReferenceNode(typeNode.typeName, typeArgs);
}
visitArrayType(type: ArrayType, context: Context): ts.ArrayTypeNode {
return ts.createArrayTypeNode(this.translateType(type, context));
return ts.createArrayTypeNode(this.translateType(type.of, context));
}
visitMapType(type: MapType, context: Context): ts.TypeLiteralNode {
@ -497,8 +497,18 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
throw new Error('Method not implemented.');
}
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.LiteralTypeNode {
return ts.createLiteralTypeNode(ts.createLiteral(ast.value as string));
visitLiteralExpr(ast: LiteralExpr, context: Context): ts.TypeNode {
if (ast.value === null) {
return ts.createKeywordTypeNode(ts.SyntaxKind.NullKeyword);
} else if (ast.value === undefined) {
return ts.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword);
} else if (typeof ast.value === 'boolean') {
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
} else if (typeof ast.value === 'number') {
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
} else {
return ts.createLiteralTypeNode(ts.createLiteral(ast.value));
}
}
visitLocalizedString(ast: LocalizedString, context: Context): never {
@ -578,6 +588,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
return ts.createTypeReferenceNode(node, /* typeArguments */ undefined);
} else if (ts.isTypeNode(node)) {
return node;
} else if (ts.isLiteralExpression(node)) {
return ts.createLiteralTypeNode(node);
} else {
throw new Error(
`Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`);
@ -590,8 +602,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
return ts.createTypeQueryNode(expr as ts.Identifier);
}
private translateType(expr: Type, context: Context): ts.TypeNode {
const typeNode = expr.visitType(this, context);
private translateType(type: Type, context: Context): ts.TypeNode {
const typeNode = type.visitType(this, context);
if (!ts.isTypeNode(typeNode)) {
throw new Error(
`A Type must translate to a TypeNode, but was ${ts.SyntaxKind[typeNode.kind]}`);