refactor(compiler-cli): track member metadata using output AST (#63957) (#64317)

Reworks the logic that tracks the decorator metadata for members to do so using the output AST, rather than wrapping the TypeScript AST. This makes it easier to programmatically generate new members that weren't part of the TypeScript AST before.

PR Close #63957

PR Close #64317
This commit is contained in:
Kristiyan Kostadinov 2025-09-17 14:06:25 +02:00 committed by Andrew Kushnir
parent 2c79ca0b57
commit bb241ee28b

View file

@ -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,
@ -19,6 +19,7 @@ import {
import ts from 'typescript';
import {
ClassMember,
ClassMemberAccessLevel,
CtorParameter,
DeclarationNode,
@ -88,15 +89,32 @@ 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) {
if (member.decorators !== null && member.decorators.length > 0) {
decoratedMembers.push({
key: member.name,
quoted: false,
value: decoratedClassMemberToMetadata(member.decorators!, isCore),
});
if (seenMemberNames.has(member.name)) {
duplicateDecoratedMembers ??= [];
duplicateDecoratedMembers.push(member);
} else {
seenMemberNames.add(member.name);
}
}
}
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.
@ -107,13 +125,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 {
@ -153,16 +167,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);
}
/**