Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 47 additions & 19 deletions packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics';
import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics';
import {
ArrowFunctionExpr,
Expression,
Expand All @@ -19,6 +19,7 @@ import {
import ts from 'typescript';

import {
ClassMember,
ClassMemberAccessLevel,
CtorParameter,
DeclarationNode,
Expand All @@ -29,6 +30,9 @@ import {

import {valueReferenceToExpression, wrapFunctionExpressionsInParens} from './util';

/** Function that extracts metadata from an undercorated class member. */
export type UndecoratedMetadataExtractor = (member: ClassMember) => LiteralArrayExpr | null;

/**
* Given a class declaration, generate a call to `setClassMetadata` with the Angular metadata
* present on the class or its member fields. An ngDevMode guard is used to allow the call to be
Expand All @@ -43,6 +47,7 @@ export function extractClassMetadata(
isCore: boolean,
annotateForClosureCompiler?: boolean,
angularDecoratorTransform: (dec: Decorator) => Decorator = (dec) => dec,
undecoratedMetadataExtractor: UndecoratedMetadataExtractor = () => null,
): R3ClassMetadata | null {
if (!reflection.isClass(clazz)) {
return null;
Expand Down Expand Up @@ -88,15 +93,44 @@ export function extractClassMetadata(
const classMembers = reflection.getMembersOfClass(clazz).filter(
(member) =>
!member.isStatic &&
member.decorators !== null &&
member.decorators.length > 0 &&
// Private fields are not supported in the metadata emit
member.accessLevel !== ClassMemberAccessLevel.EcmaScriptPrivate,
);
const duplicateDecoratedMembers = classMembers.filter(
(member, i, arr) => arr.findIndex((arrayMember) => arrayMember.name === member.name) < i,
);
if (duplicateDecoratedMembers.length > 0) {

const decoratedMembers: {key: string; value: Expression; quoted: boolean}[] = [];
const seenMemberNames = new Set<string>();
let duplicateDecoratedMembers: ClassMember[] | null = null;

for (const member of classMembers) {
const shouldQuoteName = member.nameNode !== null && ts.isStringLiteralLike(member.nameNode);

if (member.decorators !== null && member.decorators.length > 0) {
decoratedMembers.push({
key: member.name,
quoted: shouldQuoteName,
value: decoratedClassMemberToMetadata(member.decorators!, isCore),
});

if (seenMemberNames.has(member.name)) {
duplicateDecoratedMembers ??= [];
duplicateDecoratedMembers.push(member);
} else {
seenMemberNames.add(member.name);
}
} else {
const undecoratedMetadata = undecoratedMetadataExtractor(member);

if (undecoratedMetadata !== null) {
decoratedMembers.push({
key: member.name,
quoted: shouldQuoteName,
value: undecoratedMetadata,
});
}
}
}

if (duplicateDecoratedMembers !== null) {
// This should theoretically never happen, because the only way to have duplicate instance
// member names is getter/setter pairs and decorators cannot appear in both a getter and the
// corresponding setter.
Expand All @@ -107,13 +141,9 @@ export function extractClassMetadata(
duplicateDecoratedMembers.map((member) => member.name).join(', '),
);
}
const decoratedMembers = classMembers.map((member) =>
classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore),
);

if (decoratedMembers.length > 0) {
metaPropDecorators = new WrappedNodeExpr(
ts.factory.createObjectLiteralExpression(decoratedMembers),
);
metaPropDecorators = literalMap(decoratedMembers);
}

return {
Expand Down Expand Up @@ -153,16 +183,14 @@ function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Express
/**
* Convert a reflected class member to metadata.
*/
function classMemberToMetadata(
name: ts.PropertyName | string,
function decoratedClassMemberToMetadata(
decorators: Decorator[],
isCore: boolean,
): ts.PropertyAssignment {
): LiteralArrayExpr {
const ngDecorators = decorators
.filter((dec) => isAngularDecorator(dec, isCore))
.map((decorator: Decorator) => decoratorToMetadata(decorator));
const decoratorMeta = ts.factory.createArrayLiteralExpression(ngDecorators);
return ts.factory.createPropertyAssignment(name, decoratorMeta);
.map((decorator: Decorator) => new WrappedNodeExpr(decoratorToMetadata(decorator)));
return new LiteralArrayExpr(ngDecorators);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ runInEachFileSystem(() => {
}
`);
expect(res).toContain(
`{ 'has-dashes-in-name': [{ type: Input }], noDashesInName: [{ type: Input }] })`,
`{ "has-dashes-in-name": [{ type: Input }], noDashesInName: [{ type: Input }] })`,
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,14 @@ import {
ResourceLoader,
toFactoryMetadata,
tryUnwrapForwardRef,
UndecoratedMetadataExtractor,
validateHostDirectives,
wrapFunctionExpressionsInParens,
} from '../../common';
import {
extractDirectiveMetadata,
extractHostBindingResources,
getDirectiveUndecoratedMetadataExtractor,
parseDirectiveStyles,
} from '../../directive';
import {createModuleWithProvidersResolver, NgModuleSymbol} from '../../ng_module';
Expand Down Expand Up @@ -291,6 +293,11 @@ export class ComponentDecoratorHandler
preserveSignificantWhitespace: this.i18nPreserveSignificantWhitespace,
};

this.undecoratedMetadataExtractor = getDirectiveUndecoratedMetadataExtractor(
reflector,
importTracker,
);

// Dependencies can't be deferred during HMR, because the HMR update module can't have
// dynamic imports and its dependencies need to be passed in directly. If dependencies
// are deferred, their imports will be deleted so we may lose the reference to them.
Expand All @@ -299,6 +306,7 @@ export class ComponentDecoratorHandler

private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
private elementSchemaRegistry = new DomElementSchemaRegistry();
private readonly undecoratedMetadataExtractor: UndecoratedMetadataExtractor;

/**
* During the asynchronous preanalyze phase, it's necessary to parse the template to extract
Expand Down Expand Up @@ -975,6 +983,7 @@ export class ComponentDecoratorHandler
this.isCore,
this.annotateForClosureCompiler,
(dec) => transformDecoratorResources(dec, component, styles, template),
this.undecoratedMetadataExtractor,
)
: null,
classDebugInfo: extractClassDebugInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ import {
ReferencesRegistry,
resolveProvidersRequiringFactory,
toFactoryMetadata,
UndecoratedMetadataExtractor,
validateHostDirectives,
} from '../../common';

import {extractDirectiveMetadata, extractHostBindingResources, HostBindingNodes} from './shared';
import {
extractDirectiveMetadata,
extractHostBindingResources,
getDirectiveUndecoratedMetadataExtractor,
HostBindingNodes,
} from './shared';
import {DirectiveSymbol} from './symbol';
import {JitDeclarationRegistry} from '../../common/src/jit_declaration_registry';
import {
Expand Down Expand Up @@ -154,10 +160,16 @@ export class DirectiveDecoratorHandler
private readonly usePoisonedData: boolean,
private readonly typeCheckHostBindings: boolean,
private readonly emitDeclarationOnly: boolean,
) {}
) {
this.undecoratedMetadataExtractor = getDirectiveUndecoratedMetadataExtractor(
reflector,
importTracker,
);
}

readonly precedence = HandlerPrecedence.PRIMARY;
readonly name = 'DirectiveDecoratorHandler';
private readonly undecoratedMetadataExtractor: UndecoratedMetadataExtractor;

detect(
node: ClassDeclaration,
Expand Down Expand Up @@ -240,7 +252,14 @@ export class DirectiveDecoratorHandler
hostDirectives: directiveResult.hostDirectives,
rawHostDirectives: directiveResult.rawHostDirectives,
classMetadata: this.includeClassMetadata
? extractClassMetadata(node, this.reflector, this.isCore, this.annotateForClosureCompiler)
? extractClassMetadata(
node,
this.reflector,
this.isCore,
this.annotateForClosureCompiler,
undefined,
this.undecoratedMetadataExtractor,
)
: null,
baseClass: readBaseClass(node, this.reflector, this.evaluator),
typeCheckMeta: extractDirectiveTypeCheckMeta(node, directiveResult.inputs, this.reflector),
Expand Down
140 changes: 140 additions & 0 deletions packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
ExternalReference,
ForwardRefHandling,
getSafePropertyAccessString,
LiteralArrayExpr,
literalMap,
MaybeForwardRefExpression,
ParsedHostBindings,
ParseError,
Expand All @@ -24,7 +26,10 @@ import {
R3QueryMetadata,
R3Reference,
verifyHostBindings,
R3Identifiers,
ArrowFunctionExpr,
WrappedNodeExpr,
literal,
} from '@angular/compiler';
import ts from 'typescript';

Expand Down Expand Up @@ -76,6 +81,7 @@ import {
ReferencesRegistry,
toR3Reference,
tryUnwrapForwardRef,
UndecoratedMetadataExtractor,
unwrapConstructorDependencies,
unwrapExpression,
validateConstructorDependencies,
Expand Down Expand Up @@ -855,6 +861,140 @@ export function parseFieldStringArrayValue(
return value;
}

/**
* Returns a function that can be used to extract data for the `setClassMetadata`
* calls from undecorated directive class members.
*/
export function getDirectiveUndecoratedMetadataExtractor(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we could have built up this data synthetically afte all signal inputs, queries, model etc are known? Mostly my hope would be to avoid having to call these tryParseX methods again?

Did you also explore re-using some of the logic from the JIT transforms out of curiosity?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The annoying part here is that it's dealing with the output AST, rather than JS data types or the TS AST. Originally I wanted to reuse the metadata we already have on inputs, but it ended up being easier to hook into the output AST construction instead.

reflector: ReflectionHost,
importTracker: ImportedSymbolsTracker,
): UndecoratedMetadataExtractor {
return (member: ClassMember): LiteralArrayExpr | null => {
const input = tryParseSignalInputMapping(member, reflector, importTracker);
if (input !== null) {
return getDecoratorMetaArray([
[new ExternalExpr(R3Identifiers.inputDecorator), memberMetadataFromSignalInput(input)],
]);
}

const output = tryParseInitializerBasedOutput(member, reflector, importTracker);
if (output !== null) {
return getDecoratorMetaArray([
[
new ExternalExpr(R3Identifiers.outputDecorator),
memberMetadataFromInitializerOutput(output.metadata),
],
]);
}

const model = tryParseSignalModelMapping(member, reflector, importTracker);
if (model !== null) {
return getDecoratorMetaArray([
[
new ExternalExpr(R3Identifiers.inputDecorator),
memberMetadataFromSignalInput(model.input),
],
[
new ExternalExpr(R3Identifiers.outputDecorator),
memberMetadataFromInitializerOutput(model.output),
],
]);
}

const query = tryParseSignalQueryFromInitializer(member, reflector, importTracker);
if (query !== null) {
let identifier: ExternalReference;
if (query.name === 'viewChild') {
identifier = R3Identifiers.viewChildDecorator;
} else if (query.name === 'viewChildren') {
identifier = R3Identifiers.viewChildrenDecorator;
} else if (query.name === 'contentChild') {
identifier = R3Identifiers.contentChildDecorator;
} else if (query.name === 'contentChildren') {
identifier = R3Identifiers.contentChildrenDecorator;
} else {
return null;
}

return getDecoratorMetaArray([
[new ExternalExpr(identifier), memberMetadataFromSignalQuery(query.call)],
]);
}

return null;
};
}

function getDecoratorMetaArray(
decorators: [type: ExternalExpr, args: LiteralArrayExpr][],
): LiteralArrayExpr {
return new LiteralArrayExpr(
decorators.map(([type, args]) =>
literalMap([
{key: 'type', value: type, quoted: false},
{key: 'args', value: args, quoted: false},
]),
),
);
}

function memberMetadataFromSignalInput(input: InputMapping): LiteralArrayExpr {
// Note that for signal inputs the transform is captured in the signal
// initializer so we don't need to capture it here.
return new LiteralArrayExpr([
literalMap([
{
key: 'isSignal',
value: literal(true),
quoted: false,
},
{
key: 'alias',
value: literal(input.bindingPropertyName),
quoted: false,
},
{
key: 'required',
value: literal(input.required),
quoted: false,
},
]),
]);
}

function memberMetadataFromInitializerOutput(output: InputOrOutput): LiteralArrayExpr {
return new LiteralArrayExpr([literal(output.bindingPropertyName)]);
}

function memberMetadataFromSignalQuery(call: ts.CallExpression): LiteralArrayExpr {
const firstArg = call.arguments[0];
const firstArgMeta =
ts.isStringLiteralLike(firstArg) || ts.isCallExpression(firstArg)
? new WrappedNodeExpr(firstArg)
: // If the first argument is a class reference, we need to wrap it in a `forwardRef`
// because the reference might occur after the current class. This wouldn't be flagged
// on the query initializer, because it executes after the class is initialized, whereas
// `setClassMetadata` runs immediately.
new ExternalExpr(R3Identifiers.forwardRef).callFn([
new ArrowFunctionExpr([], new WrappedNodeExpr(firstArg)),
]);

const entries: Expression[] = [
// We use wrapped nodes here, because the output AST doesn't support spread assignments.
firstArgMeta,
new WrappedNodeExpr(
ts.factory.createObjectLiteralExpression([
...(call.arguments.length > 1
? [ts.factory.createSpreadAssignment(call.arguments[1])]
: []),
ts.factory.createPropertyAssignment('isSignal', ts.factory.createTrue()),
]),
),
];

return new LiteralArrayExpr(entries);
}

function isStringArrayOrDie(value: any, name: string, node: ts.Expression): value is string[] {
if (!Array.isArray(value)) {
return false;
Expand Down
Loading