mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
1934 lines
62 KiB
TypeScript
1934 lines
62 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import {
|
|
createMayBeForwardRefExpression,
|
|
emitDistinctChangesOnlyDefaultValue,
|
|
Expression,
|
|
ExternalExpr,
|
|
ExternalReference,
|
|
ForwardRefHandling,
|
|
getSafePropertyAccessString,
|
|
MaybeForwardRefExpression,
|
|
ParsedHostBindings,
|
|
ParseError,
|
|
parseHostBindings,
|
|
R3DirectiveMetadata,
|
|
R3HostDirectiveMetadata,
|
|
R3InputMetadata,
|
|
R3QueryMetadata,
|
|
R3Reference,
|
|
verifyHostBindings,
|
|
WrappedNodeExpr,
|
|
} from '@angular/compiler';
|
|
import ts from 'typescript';
|
|
|
|
import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics';
|
|
import {
|
|
assertSuccessfulReferenceEmit,
|
|
ImportedSymbolsTracker,
|
|
ImportFlags,
|
|
Reference,
|
|
ReferenceEmitter,
|
|
} from '../../../imports';
|
|
import {
|
|
ClassPropertyMapping,
|
|
DecoratorInputTransform,
|
|
HostDirectiveMeta,
|
|
InputMapping,
|
|
InputOrOutput,
|
|
isHostDirectiveMetaForGlobalMode,
|
|
Resource,
|
|
} from '../../../metadata';
|
|
import {
|
|
DynamicValue,
|
|
EnumValue,
|
|
ForeignFunctionResolver,
|
|
PartialEvaluator,
|
|
ResolvedValue,
|
|
traceDynamicValue,
|
|
} from '../../../partial_evaluator';
|
|
import {
|
|
AmbientImport,
|
|
ClassDeclaration,
|
|
ClassMember,
|
|
ClassMemberKind,
|
|
Decorator,
|
|
filterToMembersWithDecorator,
|
|
isNamedClassDeclaration,
|
|
ReflectionHost,
|
|
reflectObjectLiteral,
|
|
} from '../../../reflection';
|
|
import {CompilationMode} from '../../../transform';
|
|
import {
|
|
assertLocalCompilationUnresolvedConst,
|
|
createForwardRefResolver,
|
|
createSourceSpan,
|
|
createValueHasWrongTypeError,
|
|
getAngularDecorators,
|
|
getConstructorDependencies,
|
|
isAngularDecorator,
|
|
ReferencesRegistry,
|
|
toR3Reference,
|
|
tryUnwrapForwardRef,
|
|
unwrapConstructorDependencies,
|
|
unwrapExpression,
|
|
validateConstructorDependencies,
|
|
wrapFunctionExpressionsInParens,
|
|
wrapTypeReference,
|
|
} from '../../common';
|
|
|
|
import {tryParseSignalInputMapping} from './input_function';
|
|
import {tryParseSignalModelMapping} from './model_function';
|
|
import {tryParseInitializerBasedOutput} from './output_function';
|
|
import {tryParseSignalQueryFromInitializer} from './query_functions';
|
|
|
|
const EMPTY_OBJECT: {[key: string]: string} = {};
|
|
|
|
type QueryDecoratorName = 'ViewChild' | 'ViewChildren' | 'ContentChild' | 'ContentChildren';
|
|
export const queryDecoratorNames: QueryDecoratorName[] = [
|
|
'ViewChild',
|
|
'ViewChildren',
|
|
'ContentChild',
|
|
'ContentChildren',
|
|
];
|
|
|
|
export interface HostBindingNodes {
|
|
literal: ts.ObjectLiteralExpression | null;
|
|
bindingDecorators: Set<ts.Decorator>;
|
|
listenerDecorators: Set<ts.Decorator>;
|
|
}
|
|
|
|
const QUERY_TYPES = new Set<string>(queryDecoratorNames);
|
|
|
|
/**
|
|
* Helper function to extract metadata from a `Directive` or `Component`. `Directive`s without a
|
|
* selector are allowed to be used for abstract base classes. These abstract directives should not
|
|
* appear in the declarations of an `NgModule` and additional verification is done when processing
|
|
* the module.
|
|
*/
|
|
export function extractDirectiveMetadata(
|
|
clazz: ClassDeclaration,
|
|
decorator: Readonly<Decorator>,
|
|
reflector: ReflectionHost,
|
|
importTracker: ImportedSymbolsTracker,
|
|
evaluator: PartialEvaluator,
|
|
refEmitter: ReferenceEmitter,
|
|
referencesRegistry: ReferencesRegistry,
|
|
isCore: boolean,
|
|
annotateForClosureCompiler: boolean,
|
|
compilationMode: CompilationMode,
|
|
defaultSelector: string | null,
|
|
strictStandalone: boolean,
|
|
implicitStandaloneValue: boolean,
|
|
emitDeclarationOnly: boolean,
|
|
):
|
|
| {
|
|
jitForced: false;
|
|
decorator: Map<string, ts.Expression>;
|
|
metadata: R3DirectiveMetadata;
|
|
inputs: ClassPropertyMapping<InputMapping>;
|
|
outputs: ClassPropertyMapping;
|
|
isStructural: boolean;
|
|
hostDirectives: HostDirectiveMeta[] | null;
|
|
rawHostDirectives: ts.Expression | null;
|
|
inputFieldNamesFromMetadataArray: Set<string>;
|
|
hostBindingNodes: HostBindingNodes;
|
|
}
|
|
| {jitForced: true} {
|
|
let directive: Map<string, ts.Expression>;
|
|
if (decorator.args === null || decorator.args.length === 0) {
|
|
directive = new Map<string, ts.Expression>();
|
|
} else if (decorator.args.length !== 1) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
decorator.node,
|
|
`Incorrect number of arguments to @${decorator.name} decorator`,
|
|
);
|
|
} else {
|
|
const meta = unwrapExpression(decorator.args[0]);
|
|
if (!ts.isObjectLiteralExpression(meta)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
|
|
meta,
|
|
`@${decorator.name} argument must be an object literal`,
|
|
);
|
|
}
|
|
directive = reflectObjectLiteral(meta);
|
|
}
|
|
|
|
if (directive.has('jit')) {
|
|
// The only allowed value is true, so there's no need to expand further.
|
|
return {jitForced: true};
|
|
}
|
|
|
|
const members = reflector.getMembersOfClass(clazz);
|
|
|
|
// Precompute a list of ts.ClassElements that have decorators. This includes things like @Input,
|
|
// @Output, @HostBinding, etc.
|
|
const decoratedElements = members.filter(
|
|
(member) => !member.isStatic && member.decorators !== null,
|
|
);
|
|
|
|
const coreModule = isCore ? undefined : '@angular/core';
|
|
|
|
// Construct the map of inputs both from the @Directive/@Component
|
|
// decorator, and the decorated fields.
|
|
const inputsFromMeta = parseInputsArray(
|
|
clazz,
|
|
directive,
|
|
evaluator,
|
|
reflector,
|
|
refEmitter,
|
|
compilationMode,
|
|
emitDeclarationOnly,
|
|
);
|
|
const inputsFromFields = parseInputFields(
|
|
clazz,
|
|
members,
|
|
evaluator,
|
|
reflector,
|
|
importTracker,
|
|
refEmitter,
|
|
isCore,
|
|
compilationMode,
|
|
inputsFromMeta,
|
|
decorator,
|
|
emitDeclarationOnly,
|
|
);
|
|
const inputs = ClassPropertyMapping.fromMappedObject({...inputsFromMeta, ...inputsFromFields});
|
|
|
|
// And outputs.
|
|
const outputsFromMeta = parseOutputsArray(directive, evaluator);
|
|
const outputsFromFields = parseOutputFields(
|
|
clazz,
|
|
decorator,
|
|
members,
|
|
isCore,
|
|
reflector,
|
|
importTracker,
|
|
evaluator,
|
|
outputsFromMeta,
|
|
);
|
|
const outputs = ClassPropertyMapping.fromMappedObject({...outputsFromMeta, ...outputsFromFields});
|
|
|
|
// Parse queries of fields.
|
|
const {viewQueries, contentQueries} = parseQueriesOfClassFields(
|
|
members,
|
|
reflector,
|
|
importTracker,
|
|
evaluator,
|
|
isCore,
|
|
);
|
|
|
|
if (directive.has('queries')) {
|
|
const signalQueryFields = new Set(
|
|
[...viewQueries, ...contentQueries].filter((q) => q.isSignal).map((q) => q.propertyName),
|
|
);
|
|
const queriesFromDecorator = extractQueriesFromDecorator(
|
|
directive.get('queries')!,
|
|
reflector,
|
|
evaluator,
|
|
isCore,
|
|
);
|
|
|
|
// Checks if the query is already declared/reserved via class members declaration.
|
|
// If so, we throw a fatal diagnostic error to prevent this unintentional pattern.
|
|
const checkAndUnwrapQuery = (q: {expr: ts.Expression; metadata: R3QueryMetadata}) => {
|
|
if (signalQueryFields.has(q.metadata.propertyName)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION,
|
|
q.expr,
|
|
`Query is declared multiple times. "@${decorator.name}" declares a query for the same property.`,
|
|
);
|
|
}
|
|
return q.metadata;
|
|
};
|
|
|
|
contentQueries.push(...queriesFromDecorator.content.map((q) => checkAndUnwrapQuery(q)));
|
|
viewQueries.push(...queriesFromDecorator.view.map((q) => checkAndUnwrapQuery(q)));
|
|
}
|
|
|
|
// Parse the selector.
|
|
let selector = defaultSelector;
|
|
if (directive.has('selector')) {
|
|
const expr = directive.get('selector')!;
|
|
const resolved = evaluator.evaluate(expr);
|
|
assertLocalCompilationUnresolvedConst(
|
|
compilationMode,
|
|
resolved,
|
|
null,
|
|
'Unresolved identifier found for @Component.selector field! Did you ' +
|
|
'import this identifier from a file outside of the compilation unit? ' +
|
|
'This is not allowed when Angular compiler runs in local mode. Possible ' +
|
|
'solutions: 1) Move the declarations into a file within the compilation ' +
|
|
'unit, 2) Inline the selector',
|
|
);
|
|
if (typeof resolved !== 'string') {
|
|
throw createValueHasWrongTypeError(expr, resolved, `selector must be a string`);
|
|
}
|
|
// use default selector in case selector is an empty string
|
|
selector = resolved === '' ? defaultSelector : resolved;
|
|
if (!selector) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DIRECTIVE_MISSING_SELECTOR,
|
|
expr,
|
|
`Directive ${clazz.name.text} has no selector, please add it!`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const hostBindingNodes: HostBindingNodes = {
|
|
literal: null,
|
|
bindingDecorators: new Set<ts.Decorator>(),
|
|
listenerDecorators: new Set<ts.Decorator>(),
|
|
};
|
|
|
|
const host = extractHostBindings(
|
|
decoratedElements,
|
|
evaluator,
|
|
coreModule,
|
|
compilationMode,
|
|
hostBindingNodes,
|
|
directive,
|
|
);
|
|
|
|
const providers: Expression | null = directive.has('providers')
|
|
? new WrappedNodeExpr(
|
|
annotateForClosureCompiler
|
|
? wrapFunctionExpressionsInParens(directive.get('providers')!)
|
|
: directive.get('providers')!,
|
|
)
|
|
: null;
|
|
|
|
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
|
|
const usesOnChanges = members.some(
|
|
(member) =>
|
|
!member.isStatic && member.kind === ClassMemberKind.Method && member.name === 'ngOnChanges',
|
|
);
|
|
|
|
// Parse exportAs.
|
|
let exportAs: string[] | null = null;
|
|
if (directive.has('exportAs')) {
|
|
const expr = directive.get('exportAs')!;
|
|
const resolved = evaluator.evaluate(expr);
|
|
|
|
assertLocalCompilationUnresolvedConst(
|
|
compilationMode,
|
|
resolved,
|
|
null,
|
|
'Unresolved identifier found for exportAs field! Did you import this ' +
|
|
'identifier from a file outside of the compilation unit? This is not ' +
|
|
'allowed when Angular compiler runs in local mode. Possible solutions: ' +
|
|
'1) Move the declarations into a file within the compilation unit, ' +
|
|
'2) Inline the selector',
|
|
);
|
|
|
|
if (typeof resolved !== 'string') {
|
|
throw createValueHasWrongTypeError(expr, resolved, `exportAs must be a string`);
|
|
}
|
|
exportAs = resolved.split(',').map((part) => part.trim());
|
|
}
|
|
|
|
const rawCtorDeps = getConstructorDependencies(clazz, reflector, isCore);
|
|
|
|
// Non-abstract directives (those with a selector) require valid constructor dependencies, whereas
|
|
// abstract directives are allowed to have invalid dependencies, given that a subclass may call
|
|
// the constructor explicitly.
|
|
const ctorDeps =
|
|
selector !== null
|
|
? validateConstructorDependencies(clazz, rawCtorDeps)
|
|
: unwrapConstructorDependencies(rawCtorDeps);
|
|
|
|
// Structural directives must have a `TemplateRef` dependency.
|
|
const isStructural =
|
|
ctorDeps !== null &&
|
|
ctorDeps !== 'invalid' &&
|
|
ctorDeps.some(
|
|
(dep) =>
|
|
dep.token instanceof ExternalExpr &&
|
|
dep.token.value.moduleName === '@angular/core' &&
|
|
dep.token.value.name === 'TemplateRef',
|
|
);
|
|
|
|
let isStandalone = implicitStandaloneValue;
|
|
if (directive.has('standalone')) {
|
|
const expr = directive.get('standalone')!;
|
|
const resolved = evaluator.evaluate(expr);
|
|
if (typeof resolved !== 'boolean') {
|
|
throw createValueHasWrongTypeError(expr, resolved, `standalone flag must be a boolean`);
|
|
}
|
|
isStandalone = resolved;
|
|
|
|
if (!isStandalone && strictStandalone) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.NON_STANDALONE_NOT_ALLOWED,
|
|
expr,
|
|
`Only standalone components/directives are allowed when 'strictStandalone' is enabled.`,
|
|
);
|
|
}
|
|
}
|
|
let isSignal = false;
|
|
if (directive.has('signals')) {
|
|
const expr = directive.get('signals')!;
|
|
const resolved = evaluator.evaluate(expr);
|
|
if (typeof resolved !== 'boolean') {
|
|
throw createValueHasWrongTypeError(expr, resolved, `signals flag must be a boolean`);
|
|
}
|
|
isSignal = resolved;
|
|
}
|
|
|
|
// Detect if the component inherits from another class
|
|
const usesInheritance = reflector.hasBaseClass(clazz);
|
|
const sourceFile = clazz.getSourceFile();
|
|
const type = wrapTypeReference(reflector, clazz);
|
|
|
|
const rawHostDirectives = directive.get('hostDirectives') || null;
|
|
const hostDirectives =
|
|
rawHostDirectives === null
|
|
? null
|
|
: extractHostDirectives(
|
|
rawHostDirectives,
|
|
evaluator,
|
|
reflector,
|
|
compilationMode,
|
|
createForwardRefResolver(isCore),
|
|
emitDeclarationOnly,
|
|
);
|
|
|
|
if (compilationMode !== CompilationMode.LOCAL && hostDirectives !== null) {
|
|
// In global compilation mode where we do type checking, the template type-checker will need to
|
|
// import host directive types, so add them as referenced by `clazz`. This will ensure that
|
|
// libraries are required to export host directives which are visible from publicly exported
|
|
// components.
|
|
referencesRegistry.add(
|
|
clazz,
|
|
...hostDirectives.map((hostDir) => {
|
|
if (!isHostDirectiveMetaForGlobalMode(hostDir)) {
|
|
throw new Error('Impossible state');
|
|
}
|
|
|
|
return hostDir.directive;
|
|
}),
|
|
);
|
|
}
|
|
|
|
const metadata: R3DirectiveMetadata = {
|
|
name: clazz.name.text,
|
|
deps: ctorDeps,
|
|
host: {
|
|
...host,
|
|
},
|
|
lifecycle: {
|
|
usesOnChanges,
|
|
},
|
|
inputs: inputs.toJointMappedObject(toR3InputMetadata),
|
|
outputs: outputs.toDirectMappedObject(),
|
|
queries: contentQueries,
|
|
viewQueries,
|
|
selector,
|
|
fullInheritance: false,
|
|
type,
|
|
typeArgumentCount: reflector.getGenericArityOfClass(clazz) || 0,
|
|
typeSourceSpan: createSourceSpan(clazz.name),
|
|
usesInheritance,
|
|
exportAs,
|
|
providers,
|
|
isStandalone,
|
|
isSignal,
|
|
hostDirectives:
|
|
hostDirectives?.map((hostDir) => toHostDirectiveMetadata(hostDir, sourceFile, refEmitter)) ||
|
|
null,
|
|
};
|
|
return {
|
|
jitForced: false,
|
|
decorator: directive,
|
|
metadata,
|
|
inputs,
|
|
outputs,
|
|
isStructural,
|
|
hostDirectives,
|
|
rawHostDirectives,
|
|
hostBindingNodes,
|
|
// Track inputs from class metadata. This is useful for migration efforts.
|
|
inputFieldNamesFromMetadataArray: new Set(
|
|
Object.values(inputsFromMeta).map((i) => i.classPropertyName),
|
|
),
|
|
};
|
|
}
|
|
|
|
export function extractDecoratorQueryMetadata(
|
|
exprNode: ts.Node,
|
|
name: string,
|
|
args: ReadonlyArray<ts.Expression>,
|
|
propertyName: string,
|
|
reflector: ReflectionHost,
|
|
evaluator: PartialEvaluator,
|
|
): R3QueryMetadata {
|
|
if (args.length === 0) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
exprNode,
|
|
`@${name} must have arguments`,
|
|
);
|
|
}
|
|
const first = name === 'ViewChild' || name === 'ContentChild';
|
|
const forwardReferenceTarget = tryUnwrapForwardRef(args[0], reflector);
|
|
const node = forwardReferenceTarget ?? args[0];
|
|
|
|
const arg = evaluator.evaluate(node);
|
|
|
|
/** Whether or not this query should collect only static results (see view/api.ts) */
|
|
let isStatic: boolean = false;
|
|
|
|
// Extract the predicate
|
|
let predicate: MaybeForwardRefExpression | string[] | null = null;
|
|
if (arg instanceof Reference || arg instanceof DynamicValue) {
|
|
// References and predicates that could not be evaluated statically are emitted as is.
|
|
predicate = createMayBeForwardRefExpression(
|
|
new WrappedNodeExpr(node),
|
|
forwardReferenceTarget !== null ? ForwardRefHandling.Unwrapped : ForwardRefHandling.None,
|
|
);
|
|
} else if (typeof arg === 'string') {
|
|
predicate = [arg];
|
|
} else if (isStringArrayOrDie(arg, `@${name} predicate`, node)) {
|
|
predicate = arg;
|
|
} else {
|
|
throw createValueHasWrongTypeError(node, arg, `@${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';
|
|
let emitDistinctChangesOnly: boolean = emitDistinctChangesOnlyDefaultValue;
|
|
if (args.length === 2) {
|
|
const optionsExpr = unwrapExpression(args[1]);
|
|
if (!ts.isObjectLiteralExpression(optionsExpr)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
|
|
optionsExpr,
|
|
`@${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 descendantsExpr = options.get('descendants')!;
|
|
const descendantsValue = evaluator.evaluate(descendantsExpr);
|
|
if (typeof descendantsValue !== 'boolean') {
|
|
throw createValueHasWrongTypeError(
|
|
descendantsExpr,
|
|
descendantsValue,
|
|
`@${name} options.descendants must be a boolean`,
|
|
);
|
|
}
|
|
descendants = descendantsValue;
|
|
}
|
|
|
|
if (options.has('emitDistinctChangesOnly')) {
|
|
const emitDistinctChangesOnlyExpr = options.get('emitDistinctChangesOnly')!;
|
|
const emitDistinctChangesOnlyValue = evaluator.evaluate(emitDistinctChangesOnlyExpr);
|
|
if (typeof emitDistinctChangesOnlyValue !== 'boolean') {
|
|
throw createValueHasWrongTypeError(
|
|
emitDistinctChangesOnlyExpr,
|
|
emitDistinctChangesOnlyValue,
|
|
`@${name} options.emitDistinctChangesOnly must be a boolean`,
|
|
);
|
|
}
|
|
emitDistinctChangesOnly = emitDistinctChangesOnlyValue;
|
|
}
|
|
|
|
if (options.has('static')) {
|
|
const staticValue = evaluator.evaluate(options.get('static')!);
|
|
if (typeof staticValue !== 'boolean') {
|
|
throw createValueHasWrongTypeError(
|
|
node,
|
|
staticValue,
|
|
`@${name} options.static must be a boolean`,
|
|
);
|
|
}
|
|
isStatic = staticValue;
|
|
}
|
|
} else if (args.length > 2) {
|
|
// Too many arguments.
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
node,
|
|
`@${name} has too many arguments`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
isSignal: false,
|
|
propertyName,
|
|
predicate,
|
|
first,
|
|
descendants,
|
|
read,
|
|
static: isStatic,
|
|
emitDistinctChangesOnly,
|
|
};
|
|
}
|
|
|
|
function extractHostBindings(
|
|
members: ClassMember[],
|
|
evaluator: PartialEvaluator,
|
|
coreModule: string | undefined,
|
|
compilationMode: CompilationMode,
|
|
hostBindingNodes: HostBindingNodes,
|
|
metadata?: Map<string, ts.Expression>,
|
|
): ParsedHostBindings {
|
|
let bindings: ParsedHostBindings;
|
|
if (metadata && metadata.has('host')) {
|
|
const hostExpression = metadata.get('host')!;
|
|
bindings = evaluateHostExpressionBindings(hostExpression, evaluator);
|
|
if (ts.isObjectLiteralExpression(hostExpression)) {
|
|
hostBindingNodes.literal = hostExpression;
|
|
}
|
|
} else {
|
|
bindings = parseHostBindings({});
|
|
}
|
|
|
|
filterToMembersWithDecorator(members, 'HostBinding', coreModule).forEach(
|
|
({member, decorators}) => {
|
|
decorators.forEach((decorator) => {
|
|
let hostPropertyName: string = member.name;
|
|
if (decorator.args !== null && decorator.args.length > 0) {
|
|
if (decorator.args.length !== 1) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
decorator.node,
|
|
`@HostBinding can have at most one argument, got ${decorator.args.length} argument(s)`,
|
|
);
|
|
}
|
|
|
|
const resolved = evaluator.evaluate(decorator.args[0]);
|
|
|
|
// Specific error for local compilation mode if the argument cannot be resolved
|
|
assertLocalCompilationUnresolvedConst(
|
|
compilationMode,
|
|
resolved,
|
|
null,
|
|
"Unresolved identifier found for @HostBinding's argument! Did " +
|
|
'you import this identifier from a file outside of the compilation ' +
|
|
'unit? This is not allowed when Angular compiler runs in local mode. ' +
|
|
'Possible solutions: 1) Move the declaration into a file within ' +
|
|
'the compilation unit, 2) Inline the argument',
|
|
);
|
|
|
|
if (typeof resolved !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
decorator.node,
|
|
resolved,
|
|
`@HostBinding's argument must be a string`,
|
|
);
|
|
}
|
|
|
|
hostPropertyName = resolved;
|
|
}
|
|
|
|
if (ts.isDecorator(decorator.node)) {
|
|
hostBindingNodes.bindingDecorators.add(decorator.node);
|
|
}
|
|
|
|
// Since this is a decorator, we know that the value is a class member. Always access it
|
|
// through `this` so that further down the line it can't be confused for a literal value
|
|
// (e.g. if there's a property called `true`). There is no size penalty, because all
|
|
// values (except literals) are converted to `ctx.propName` eventually.
|
|
bindings.properties[hostPropertyName] = getSafePropertyAccessString('this', member.name);
|
|
});
|
|
},
|
|
);
|
|
|
|
filterToMembersWithDecorator(members, 'HostListener', coreModule).forEach(
|
|
({member, decorators}) => {
|
|
decorators.forEach((decorator) => {
|
|
let eventName: string = member.name;
|
|
let args: string[] = [];
|
|
if (decorator.args !== null && decorator.args.length > 0) {
|
|
if (decorator.args.length > 2) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
decorator.args[2],
|
|
`@HostListener can have at most two arguments`,
|
|
);
|
|
}
|
|
|
|
const resolved = evaluator.evaluate(decorator.args[0]);
|
|
|
|
// Specific error for local compilation mode if the event name cannot be resolved
|
|
assertLocalCompilationUnresolvedConst(
|
|
compilationMode,
|
|
resolved,
|
|
null,
|
|
"Unresolved identifier found for @HostListener's event name " +
|
|
'argument! Did you import this identifier from a file outside of ' +
|
|
'the compilation unit? This is not allowed when Angular compiler ' +
|
|
'runs in local mode. Possible solutions: 1) Move the declaration ' +
|
|
'into a file within the compilation unit, 2) Inline the argument',
|
|
);
|
|
|
|
if (typeof resolved !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
decorator.args[0],
|
|
resolved,
|
|
`@HostListener's event name argument must be a string`,
|
|
);
|
|
}
|
|
|
|
eventName = resolved;
|
|
|
|
if (decorator.args.length === 2) {
|
|
const expression = decorator.args[1];
|
|
const resolvedArgs = evaluator.evaluate(decorator.args[1]);
|
|
if (!isStringArrayOrDie(resolvedArgs, '@HostListener.args', expression)) {
|
|
throw createValueHasWrongTypeError(
|
|
decorator.args[1],
|
|
resolvedArgs,
|
|
`@HostListener's second argument must be a string array`,
|
|
);
|
|
}
|
|
args = resolvedArgs;
|
|
}
|
|
}
|
|
|
|
if (ts.isDecorator(decorator.node)) {
|
|
hostBindingNodes.listenerDecorators.add(decorator.node);
|
|
}
|
|
|
|
bindings.listeners[eventName] = `${member.name}(${args.join(',')})`;
|
|
});
|
|
},
|
|
);
|
|
return bindings;
|
|
}
|
|
|
|
function extractQueriesFromDecorator(
|
|
queryData: ts.Expression,
|
|
reflector: ReflectionHost,
|
|
evaluator: PartialEvaluator,
|
|
isCore: boolean,
|
|
): {
|
|
content: {expr: ts.Expression; metadata: R3QueryMetadata}[];
|
|
view: {expr: ts.Expression; metadata: R3QueryMetadata}[];
|
|
} {
|
|
const content: {expr: ts.Expression; metadata: R3QueryMetadata}[] = [];
|
|
const view: {expr: ts.Expression; metadata: R3QueryMetadata}[] = [];
|
|
|
|
if (!ts.isObjectLiteralExpression(queryData)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.VALUE_HAS_WRONG_TYPE,
|
|
queryData,
|
|
'Decorator queries metadata must be an object literal',
|
|
);
|
|
}
|
|
reflectObjectLiteral(queryData).forEach((queryExpr, propertyName) => {
|
|
queryExpr = unwrapExpression(queryExpr);
|
|
if (!ts.isNewExpression(queryExpr)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.VALUE_HAS_WRONG_TYPE,
|
|
queryData,
|
|
'Decorator query metadata must be an instance of a query type',
|
|
);
|
|
}
|
|
const queryType = ts.isPropertyAccessExpression(queryExpr.expression)
|
|
? queryExpr.expression.name
|
|
: queryExpr.expression;
|
|
if (!ts.isIdentifier(queryType)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.VALUE_HAS_WRONG_TYPE,
|
|
queryData,
|
|
'Decorator query metadata must be an instance of a query type',
|
|
);
|
|
}
|
|
const type = reflector.getImportOfIdentifier(queryType);
|
|
if (
|
|
type === null ||
|
|
(!isCore && type.from !== '@angular/core') ||
|
|
!QUERY_TYPES.has(type.name)
|
|
) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.VALUE_HAS_WRONG_TYPE,
|
|
queryData,
|
|
'Decorator query metadata must be an instance of a query type',
|
|
);
|
|
}
|
|
|
|
const query = extractDecoratorQueryMetadata(
|
|
queryExpr,
|
|
type.name,
|
|
queryExpr.arguments || [],
|
|
propertyName,
|
|
reflector,
|
|
evaluator,
|
|
);
|
|
if (type.name.startsWith('Content')) {
|
|
content.push({expr: queryExpr, metadata: query});
|
|
} else {
|
|
view.push({expr: queryExpr, metadata: query});
|
|
}
|
|
});
|
|
return {content, view};
|
|
}
|
|
|
|
export function parseDirectiveStyles(
|
|
directive: Map<string, ts.Expression>,
|
|
evaluator: PartialEvaluator,
|
|
compilationMode: CompilationMode,
|
|
): null | string[] {
|
|
const expression = directive.get('styles');
|
|
|
|
if (!expression) {
|
|
return null;
|
|
}
|
|
|
|
const evaluated = evaluator.evaluate(expression);
|
|
const value = typeof evaluated === 'string' ? [evaluated] : evaluated;
|
|
|
|
// Check if the identifier used for @Component.styles cannot be resolved in local compilation
|
|
// mode. if the case, an error specific to this situation is generated.
|
|
if (compilationMode === CompilationMode.LOCAL) {
|
|
let unresolvedNode: ts.Node | null = null;
|
|
if (Array.isArray(value)) {
|
|
const entry = value.find((e) => e instanceof DynamicValue && e.isFromUnknownIdentifier()) as
|
|
| DynamicValue
|
|
| undefined;
|
|
unresolvedNode = entry?.node ?? null;
|
|
} else if (value instanceof DynamicValue && value.isFromUnknownIdentifier()) {
|
|
unresolvedNode = value.node;
|
|
}
|
|
|
|
if (unresolvedNode !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.LOCAL_COMPILATION_UNRESOLVED_CONST,
|
|
unresolvedNode,
|
|
'Unresolved identifier found for @Component.styles field! Did you import ' +
|
|
'this identifier from a file outside of the compilation unit? This is ' +
|
|
'not allowed when Angular compiler runs in local mode. Possible ' +
|
|
'solutions: 1) Move the declarations into a file within the compilation ' +
|
|
'unit, 2) Inline the styles, 3) Move the styles into separate files and ' +
|
|
'include it using @Component.styleUrls',
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!isStringArrayOrDie(value, 'styles', expression)) {
|
|
throw createValueHasWrongTypeError(
|
|
expression,
|
|
value,
|
|
`Failed to resolve @Component.styles to a string or an array of strings`,
|
|
);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
export function parseFieldStringArrayValue(
|
|
directive: Map<string, ts.Expression>,
|
|
field: string,
|
|
evaluator: PartialEvaluator,
|
|
): null | string[] {
|
|
if (!directive.has(field)) {
|
|
return null;
|
|
}
|
|
|
|
// Resolve the field of interest from the directive metadata to a string[].
|
|
const expression = directive.get(field)!;
|
|
const value = evaluator.evaluate(expression);
|
|
if (!isStringArrayOrDie(value, field, expression)) {
|
|
throw createValueHasWrongTypeError(
|
|
expression,
|
|
value,
|
|
`Failed to resolve @Directive.${field} to a string array`,
|
|
);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function isStringArrayOrDie(value: any, name: string, node: ts.Expression): value is string[] {
|
|
if (!Array.isArray(value)) {
|
|
return false;
|
|
}
|
|
|
|
for (let i = 0; i < value.length; i++) {
|
|
if (typeof value[i] !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
node,
|
|
value[i],
|
|
`Failed to resolve ${name} at position ${i} to a string`,
|
|
);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function tryGetQueryFromFieldDecorator(
|
|
member: ClassMember,
|
|
reflector: ReflectionHost,
|
|
evaluator: PartialEvaluator,
|
|
isCore: boolean,
|
|
): {name: QueryDecoratorName; decorator: Decorator; metadata: R3QueryMetadata} | null {
|
|
const decorators = member.decorators;
|
|
if (decorators === null) {
|
|
return null;
|
|
}
|
|
|
|
const queryDecorators = getAngularDecorators(decorators, queryDecoratorNames, isCore);
|
|
if (queryDecorators.length === 0) {
|
|
return null;
|
|
}
|
|
if (queryDecorators.length !== 1) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_COLLISION,
|
|
member.node ?? queryDecorators[0].node,
|
|
'Cannot combine multiple query decorators.',
|
|
);
|
|
}
|
|
|
|
const decorator = queryDecorators[0];
|
|
const node = member.node || decorator.node;
|
|
|
|
// Throw in case of `@Input() @ContentChild('foo') foo: any`, which is not supported in Ivy
|
|
if (decorators.some((v) => v.name === 'Input')) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_COLLISION,
|
|
node,
|
|
'Cannot combine @Input decorators with query decorators',
|
|
);
|
|
}
|
|
if (!isPropertyTypeMember(member)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_UNEXPECTED,
|
|
node,
|
|
'Query decorator must go on a property-type member',
|
|
);
|
|
}
|
|
|
|
// Either the decorator was aliased, or is referenced directly with
|
|
// the proper query name.
|
|
const name = (decorator.import?.name ?? decorator.name) as QueryDecoratorName;
|
|
|
|
return {
|
|
name,
|
|
decorator,
|
|
metadata: extractDecoratorQueryMetadata(
|
|
node,
|
|
name,
|
|
decorator.args || [],
|
|
member.name,
|
|
reflector,
|
|
evaluator,
|
|
),
|
|
};
|
|
}
|
|
|
|
function isPropertyTypeMember(member: ClassMember): boolean {
|
|
return (
|
|
member.kind === ClassMemberKind.Getter ||
|
|
member.kind === ClassMemberKind.Setter ||
|
|
member.kind === ClassMemberKind.Property
|
|
);
|
|
}
|
|
|
|
function parseMappingStringArray(values: string[]) {
|
|
return values.reduce(
|
|
(results, value) => {
|
|
if (typeof value !== 'string') {
|
|
throw new Error('Mapping value must be a string');
|
|
}
|
|
|
|
const [bindingPropertyName, fieldName] = parseMappingString(value);
|
|
results[fieldName] = bindingPropertyName;
|
|
return results;
|
|
},
|
|
{} as {[field: string]: string},
|
|
);
|
|
}
|
|
|
|
function parseMappingString(value: string): [bindingPropertyName: string, fieldName: string] {
|
|
// Either the value is 'field' or 'field: property'. In the first case, `property` will
|
|
// be undefined, in which case the field name should also be used as the property name.
|
|
const [fieldName, bindingPropertyName] = value.split(':', 2).map((str) => str.trim());
|
|
return [bindingPropertyName ?? fieldName, fieldName];
|
|
}
|
|
|
|
/** Parses the `inputs` array of a directive/component decorator. */
|
|
function parseInputsArray(
|
|
clazz: ClassDeclaration,
|
|
decoratorMetadata: Map<string, ts.Expression>,
|
|
evaluator: PartialEvaluator,
|
|
reflector: ReflectionHost,
|
|
refEmitter: ReferenceEmitter,
|
|
compilationMode: CompilationMode,
|
|
emitDeclarationOnly: boolean,
|
|
): Record<string, InputMapping> {
|
|
const inputsField = decoratorMetadata.get('inputs');
|
|
|
|
if (inputsField === undefined) {
|
|
return {};
|
|
}
|
|
|
|
const inputs = {} as Record<string, InputMapping>;
|
|
const inputsArray = evaluator.evaluate(inputsField);
|
|
|
|
if (!Array.isArray(inputsArray)) {
|
|
throw createValueHasWrongTypeError(
|
|
inputsField,
|
|
inputsArray,
|
|
`Failed to resolve @Directive.inputs to an array`,
|
|
);
|
|
}
|
|
|
|
for (let i = 0; i < inputsArray.length; i++) {
|
|
const value = inputsArray[i];
|
|
|
|
if (typeof value === 'string') {
|
|
// If the value is a string, we treat it as a mapping string.
|
|
const [bindingPropertyName, classPropertyName] = parseMappingString(value);
|
|
inputs[classPropertyName] = {
|
|
bindingPropertyName,
|
|
classPropertyName,
|
|
required: false,
|
|
transform: null,
|
|
// Note: Signal inputs are not allowed with the array form.
|
|
isSignal: false,
|
|
};
|
|
} else if (value instanceof Map) {
|
|
// If it's a map, we treat it as a config object.
|
|
const name = value.get('name');
|
|
const alias = value.get('alias');
|
|
const required = value.get('required');
|
|
let transform: DecoratorInputTransform | null = null;
|
|
|
|
if (typeof name !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
inputsField,
|
|
name,
|
|
`Value at position ${i} of @Directive.inputs array must have a "name" property`,
|
|
);
|
|
}
|
|
|
|
if (value.has('transform')) {
|
|
const transformValue = value.get('transform');
|
|
|
|
if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) {
|
|
throw createValueHasWrongTypeError(
|
|
inputsField,
|
|
transformValue,
|
|
`Transform of value at position ${i} of @Directive.inputs array must be a function`,
|
|
);
|
|
}
|
|
|
|
transform = parseDecoratorInputTransformFunction(
|
|
clazz,
|
|
name,
|
|
transformValue,
|
|
reflector,
|
|
refEmitter,
|
|
compilationMode,
|
|
emitDeclarationOnly,
|
|
);
|
|
}
|
|
|
|
inputs[name] = {
|
|
classPropertyName: name,
|
|
bindingPropertyName: typeof alias === 'string' ? alias : name,
|
|
required: required === true,
|
|
// Note: Signal inputs are not allowed with the array form.
|
|
isSignal: false,
|
|
transform,
|
|
};
|
|
} else {
|
|
throw createValueHasWrongTypeError(
|
|
inputsField,
|
|
value,
|
|
`@Directive.inputs array can only contain strings or object literals`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return inputs;
|
|
}
|
|
|
|
/** Attempts to find a given Angular decorator on the class member. */
|
|
function tryGetDecoratorOnMember(
|
|
member: ClassMember,
|
|
decoratorName: string,
|
|
isCore: boolean,
|
|
): Decorator | null {
|
|
if (member.decorators === null) {
|
|
return null;
|
|
}
|
|
|
|
for (const decorator of member.decorators) {
|
|
if (isAngularDecorator(decorator, decoratorName, isCore)) {
|
|
return decorator;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function tryParseInputFieldMapping(
|
|
clazz: ClassDeclaration,
|
|
member: ClassMember,
|
|
evaluator: PartialEvaluator,
|
|
reflector: ReflectionHost,
|
|
importTracker: ImportedSymbolsTracker,
|
|
isCore: boolean,
|
|
refEmitter: ReferenceEmitter,
|
|
compilationMode: CompilationMode,
|
|
emitDeclarationOnly: boolean,
|
|
): InputMapping | null {
|
|
const classPropertyName = member.name;
|
|
|
|
const decorator = tryGetDecoratorOnMember(member, 'Input', isCore);
|
|
const signalInputMapping = tryParseSignalInputMapping(member, reflector, importTracker);
|
|
const modelInputMapping = tryParseSignalModelMapping(member, reflector, importTracker);
|
|
|
|
if (decorator !== null && signalInputMapping !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR,
|
|
decorator.node,
|
|
`Using @Input with a signal input is not allowed.`,
|
|
);
|
|
}
|
|
|
|
if (decorator !== null && modelInputMapping !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR,
|
|
decorator.node,
|
|
`Using @Input with a model input is not allowed.`,
|
|
);
|
|
}
|
|
|
|
// Check `@Input` case.
|
|
if (decorator !== null) {
|
|
if (decorator.args !== null && decorator.args.length > 1) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
decorator.node,
|
|
`@${decorator.name} can have at most one argument, got ${decorator.args.length} argument(s)`,
|
|
);
|
|
}
|
|
|
|
const optionsNode =
|
|
decorator.args !== null && decorator.args.length === 1 ? decorator.args[0] : undefined;
|
|
const options = optionsNode !== undefined ? evaluator.evaluate(optionsNode) : null;
|
|
const required = options instanceof Map ? options.get('required') === true : false;
|
|
|
|
// To preserve old behavior: Even though TypeScript types ensure proper options are
|
|
// passed, we sanity check for unsupported values here again.
|
|
if (options !== null && typeof options !== 'string' && !(options instanceof Map)) {
|
|
throw createValueHasWrongTypeError(
|
|
decorator.node,
|
|
options,
|
|
`@${decorator.name} decorator argument must resolve to a string or an object literal`,
|
|
);
|
|
}
|
|
|
|
let alias: string | null = null;
|
|
if (typeof options === 'string') {
|
|
alias = options;
|
|
} else if (options instanceof Map && typeof options.get('alias') === 'string') {
|
|
alias = options.get('alias') as string;
|
|
}
|
|
|
|
const publicInputName = alias ?? classPropertyName;
|
|
|
|
let transform: DecoratorInputTransform | null = null;
|
|
if (options instanceof Map && options.has('transform')) {
|
|
const transformValue = options.get('transform');
|
|
|
|
if (!(transformValue instanceof DynamicValue) && !(transformValue instanceof Reference)) {
|
|
throw createValueHasWrongTypeError(
|
|
optionsNode!,
|
|
transformValue,
|
|
`Input transform must be a function`,
|
|
);
|
|
}
|
|
|
|
transform = parseDecoratorInputTransformFunction(
|
|
clazz,
|
|
classPropertyName,
|
|
transformValue,
|
|
reflector,
|
|
refEmitter,
|
|
compilationMode,
|
|
emitDeclarationOnly,
|
|
);
|
|
}
|
|
|
|
return {
|
|
isSignal: false,
|
|
classPropertyName,
|
|
bindingPropertyName: publicInputName,
|
|
transform,
|
|
required,
|
|
};
|
|
}
|
|
|
|
// Look for signal inputs. e.g. `memberName = input()`
|
|
if (signalInputMapping !== null) {
|
|
return signalInputMapping;
|
|
}
|
|
|
|
if (modelInputMapping !== null) {
|
|
return modelInputMapping.input;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Parses the class members that declare inputs (via decorator or initializer). */
|
|
function parseInputFields(
|
|
clazz: ClassDeclaration,
|
|
members: ClassMember[],
|
|
evaluator: PartialEvaluator,
|
|
reflector: ReflectionHost,
|
|
importTracker: ImportedSymbolsTracker,
|
|
refEmitter: ReferenceEmitter,
|
|
isCore: boolean,
|
|
compilationMode: CompilationMode,
|
|
inputsFromClassDecorator: Record<string, InputMapping>,
|
|
classDecorator: Decorator,
|
|
emitDeclarationOnly: boolean,
|
|
): Record<string, InputMapping> {
|
|
const inputs = {} as Record<string, InputMapping>;
|
|
|
|
for (const member of members) {
|
|
const classPropertyName = member.name;
|
|
const inputMapping = tryParseInputFieldMapping(
|
|
clazz,
|
|
member,
|
|
evaluator,
|
|
reflector,
|
|
importTracker,
|
|
isCore,
|
|
refEmitter,
|
|
compilationMode,
|
|
emitDeclarationOnly,
|
|
);
|
|
if (inputMapping === null) {
|
|
continue;
|
|
}
|
|
|
|
if (member.isStatic) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER,
|
|
member.node ?? clazz,
|
|
`Input "${member.name}" is incorrectly declared as static member of "${clazz.name.text}".`,
|
|
);
|
|
}
|
|
|
|
// Validate that signal inputs are not accidentally declared in the `inputs` metadata.
|
|
if (inputMapping.isSignal && inputsFromClassDecorator.hasOwnProperty(classPropertyName)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION,
|
|
member.node ?? clazz,
|
|
`Input "${member.name}" is also declared as non-signal in @${classDecorator.name}.`,
|
|
);
|
|
}
|
|
|
|
inputs[classPropertyName] = inputMapping;
|
|
}
|
|
|
|
return inputs;
|
|
}
|
|
|
|
/**
|
|
* Parses the `transform` function and its type for a decorator `@Input`.
|
|
*
|
|
* This logic verifies feasibility of extracting the transform write type
|
|
* into a different place, so that the input write type can be captured at
|
|
* a later point in a static acceptance member.
|
|
*
|
|
* Note: This is not needed for signal inputs where the transform type is
|
|
* automatically captured in the type of the `InputSignal`.
|
|
*
|
|
*/
|
|
export function parseDecoratorInputTransformFunction(
|
|
clazz: ClassDeclaration,
|
|
classPropertyName: string,
|
|
value: DynamicValue | Reference,
|
|
reflector: ReflectionHost,
|
|
refEmitter: ReferenceEmitter,
|
|
compilationMode: CompilationMode,
|
|
emitDeclarationOnly: boolean,
|
|
): DecoratorInputTransform {
|
|
if (emitDeclarationOnly) {
|
|
const chain: ts.DiagnosticMessageChain = {
|
|
messageText:
|
|
'@Input decorators with a transform function are not supported in experimental declaration-only emission mode',
|
|
category: ts.DiagnosticCategory.Error,
|
|
code: 0,
|
|
next: [
|
|
{
|
|
messageText: `Consider converting '${clazz.name.text}.${classPropertyName}' to an input signal`,
|
|
category: ts.DiagnosticCategory.Message,
|
|
code: 0,
|
|
},
|
|
],
|
|
};
|
|
throw new FatalDiagnosticError(ErrorCode.DECORATOR_UNEXPECTED, value.node, chain);
|
|
}
|
|
// In local compilation mode we can skip type checking the function args. This is because usually
|
|
// the type check is done in a separate build which runs in full compilation mode. So here we skip
|
|
// all the diagnostics.
|
|
if (compilationMode === CompilationMode.LOCAL) {
|
|
const node =
|
|
value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;
|
|
|
|
// This should never be null since we know the reference originates
|
|
// from the same file, but we null check it just in case.
|
|
if (node === null) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function could not be referenced',
|
|
);
|
|
}
|
|
|
|
return {
|
|
node,
|
|
type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
|
|
};
|
|
}
|
|
|
|
const definition = reflector.getDefinitionOfFunction(value.node);
|
|
|
|
if (definition === null) {
|
|
throw createValueHasWrongTypeError(value.node, value, 'Input transform must be a function');
|
|
}
|
|
|
|
if (definition.typeParameters !== null && definition.typeParameters.length > 0) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function cannot be generic',
|
|
);
|
|
}
|
|
|
|
if (definition.signatureCount > 1) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function cannot have multiple signatures',
|
|
);
|
|
}
|
|
|
|
const members = reflector.getMembersOfClass(clazz);
|
|
|
|
for (const member of members) {
|
|
const conflictingName = `ngAcceptInputType_${classPropertyName}`;
|
|
|
|
if (member.name === conflictingName && member.isStatic) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.CONFLICTING_INPUT_TRANSFORM,
|
|
value.node,
|
|
`Class cannot have both a transform function on Input ${classPropertyName} and a static member called ${conflictingName}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const node = value instanceof Reference ? value.getIdentityIn(clazz.getSourceFile()) : value.node;
|
|
|
|
// This should never be null since we know the reference originates
|
|
// from the same file, but we null check it just in case.
|
|
if (node === null) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function could not be referenced',
|
|
);
|
|
}
|
|
|
|
// Skip over `this` parameters since they're typing the context, not the actual parameter.
|
|
// `this` parameters are guaranteed to be first if they exist, and the only to distinguish them
|
|
// is using the name, TS doesn't have a special AST for them.
|
|
const firstParam =
|
|
definition.parameters[0]?.name === 'this' ? definition.parameters[1] : definition.parameters[0];
|
|
|
|
// Treat functions with no arguments as `unknown` since returning
|
|
// the same value from the transform function is valid.
|
|
if (!firstParam) {
|
|
return {
|
|
node,
|
|
type: new Reference(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
|
|
};
|
|
}
|
|
|
|
// This should be caught by `noImplicitAny` already, but null check it just in case.
|
|
if (!firstParam.type) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function first parameter must have a type',
|
|
);
|
|
}
|
|
|
|
if (firstParam.node.dotDotDotToken) {
|
|
throw createValueHasWrongTypeError(
|
|
value.node,
|
|
value,
|
|
'Input transform function first parameter cannot be a spread parameter',
|
|
);
|
|
}
|
|
|
|
assertEmittableInputType(firstParam.type, clazz.getSourceFile(), reflector, refEmitter);
|
|
|
|
const viaModule = value instanceof Reference ? value.bestGuessOwningModule : null;
|
|
return {node, type: new Reference(firstParam.type, viaModule)};
|
|
}
|
|
|
|
/**
|
|
* Verifies that a type and all types contained within
|
|
* it can be referenced in a specific context file.
|
|
*/
|
|
function assertEmittableInputType(
|
|
type: ts.TypeNode,
|
|
contextFile: ts.SourceFile,
|
|
reflector: ReflectionHost,
|
|
refEmitter: ReferenceEmitter,
|
|
): void {
|
|
(function walk(node: ts.Node) {
|
|
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
const declaration = reflector.getDeclarationOfIdentifier(node.typeName);
|
|
|
|
if (declaration !== null) {
|
|
// If the type is declared in a different file, we have to check that it can be imported
|
|
// into the context file. If they're in the same file, we need to verify that they're
|
|
// exported, otherwise TS won't emit it to the .d.ts.
|
|
if (declaration.node.getSourceFile() !== contextFile) {
|
|
const emittedType = refEmitter.emit(
|
|
new Reference(
|
|
declaration.node,
|
|
declaration.viaModule === AmbientImport ? AmbientImport : null,
|
|
),
|
|
contextFile,
|
|
ImportFlags.NoAliasing |
|
|
ImportFlags.AllowTypeImports |
|
|
ImportFlags.AllowRelativeDtsImports |
|
|
ImportFlags.AllowAmbientReferences,
|
|
);
|
|
|
|
assertSuccessfulReferenceEmit(emittedType, node, 'type');
|
|
} else if (!reflector.isStaticallyExported(declaration.node)) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.SYMBOL_NOT_EXPORTED,
|
|
type,
|
|
`Symbol must be exported in order to be used as the type of an Input transform function`,
|
|
[makeRelatedInformation(declaration.node, `The symbol is declared here.`)],
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
node.forEachChild(walk);
|
|
})(type);
|
|
}
|
|
|
|
/**
|
|
* Iterates through all specified class members and attempts to detect
|
|
* view and content queries defined.
|
|
*
|
|
* Queries may be either defined via decorators, or through class member
|
|
* initializers for signal-based queries.
|
|
*/
|
|
function parseQueriesOfClassFields(
|
|
members: ClassMember[],
|
|
reflector: ReflectionHost,
|
|
importTracker: ImportedSymbolsTracker,
|
|
evaluator: PartialEvaluator,
|
|
isCore: boolean,
|
|
): {
|
|
viewQueries: R3QueryMetadata[];
|
|
contentQueries: R3QueryMetadata[];
|
|
} {
|
|
const viewQueries: R3QueryMetadata[] = [];
|
|
const contentQueries: R3QueryMetadata[] = [];
|
|
|
|
// For backwards compatibility, decorator-based queries are grouped and
|
|
// ordered in a specific way. The order needs to match with what we had in:
|
|
// https://github.com/angular/angular/blob/8737544d6963bf664f752de273e919575cca08ac/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts#L94-L111.
|
|
const decoratorViewChild: R3QueryMetadata[] = [];
|
|
const decoratorViewChildren: R3QueryMetadata[] = [];
|
|
const decoratorContentChild: R3QueryMetadata[] = [];
|
|
const decoratorContentChildren: R3QueryMetadata[] = [];
|
|
|
|
for (const member of members) {
|
|
const decoratorQuery = tryGetQueryFromFieldDecorator(member, reflector, evaluator, isCore);
|
|
const signalQuery = tryParseSignalQueryFromInitializer(member, reflector, importTracker);
|
|
|
|
if (decoratorQuery !== null && signalQuery !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR,
|
|
decoratorQuery.decorator.node,
|
|
`Using @${decoratorQuery.name} with a signal-based query is not allowed.`,
|
|
);
|
|
}
|
|
|
|
const queryNode = decoratorQuery?.decorator.node ?? signalQuery?.call;
|
|
if (queryNode !== undefined && member.isStatic) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER,
|
|
queryNode,
|
|
`Query is incorrectly declared on a static class member.`,
|
|
);
|
|
}
|
|
|
|
if (decoratorQuery !== null) {
|
|
switch (decoratorQuery.name) {
|
|
case 'ViewChild':
|
|
decoratorViewChild.push(decoratorQuery.metadata);
|
|
break;
|
|
case 'ViewChildren':
|
|
decoratorViewChildren.push(decoratorQuery.metadata);
|
|
break;
|
|
case 'ContentChild':
|
|
decoratorContentChild.push(decoratorQuery.metadata);
|
|
break;
|
|
case 'ContentChildren':
|
|
decoratorContentChildren.push(decoratorQuery.metadata);
|
|
break;
|
|
}
|
|
} else if (signalQuery !== null) {
|
|
switch (signalQuery.name) {
|
|
case 'viewChild':
|
|
case 'viewChildren':
|
|
viewQueries.push(signalQuery.metadata);
|
|
break;
|
|
case 'contentChild':
|
|
case 'contentChildren':
|
|
contentQueries.push(signalQuery.metadata);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
viewQueries: [...viewQueries, ...decoratorViewChild, ...decoratorViewChildren],
|
|
contentQueries: [...contentQueries, ...decoratorContentChild, ...decoratorContentChildren],
|
|
};
|
|
}
|
|
|
|
/** Parses the `outputs` array of a directive/component. */
|
|
function parseOutputsArray(
|
|
directive: Map<string, ts.Expression>,
|
|
evaluator: PartialEvaluator,
|
|
): Record<string, string> {
|
|
const metaValues = parseFieldStringArrayValue(directive, 'outputs', evaluator);
|
|
return metaValues ? parseMappingStringArray(metaValues) : EMPTY_OBJECT;
|
|
}
|
|
|
|
/** Parses the class members that are outputs. */
|
|
function parseOutputFields(
|
|
clazz: ClassDeclaration,
|
|
classDecorator: Decorator,
|
|
members: ClassMember[],
|
|
isCore: boolean,
|
|
reflector: ReflectionHost,
|
|
importTracker: ImportedSymbolsTracker,
|
|
evaluator: PartialEvaluator,
|
|
outputsFromMeta: Record<string, string>,
|
|
): Record<string, string> {
|
|
const outputs = {} as Record<string, string>;
|
|
|
|
for (const member of members) {
|
|
const decoratorOutput = tryParseDecoratorOutput(member, evaluator, isCore);
|
|
const initializerOutput = tryParseInitializerBasedOutput(member, reflector, importTracker);
|
|
const modelMapping = tryParseSignalModelMapping(member, reflector, importTracker);
|
|
|
|
if (decoratorOutput !== null && initializerOutput !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR,
|
|
decoratorOutput.decorator.node,
|
|
`Using "@Output" with "output()" is not allowed.`,
|
|
);
|
|
}
|
|
|
|
if (decoratorOutput !== null && modelMapping !== null) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_WITH_DISALLOWED_DECORATOR,
|
|
decoratorOutput.decorator.node,
|
|
`Using @Output with a model input is not allowed.`,
|
|
);
|
|
}
|
|
|
|
const queryNode =
|
|
decoratorOutput?.decorator.node ?? initializerOutput?.call ?? modelMapping?.call;
|
|
if (queryNode !== undefined && member.isStatic) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INCORRECTLY_DECLARED_ON_STATIC_MEMBER,
|
|
queryNode,
|
|
`Output is incorrectly declared on a static class member.`,
|
|
);
|
|
}
|
|
|
|
let bindingPropertyName: string;
|
|
|
|
if (decoratorOutput !== null) {
|
|
bindingPropertyName = decoratorOutput.metadata.bindingPropertyName;
|
|
} else if (initializerOutput !== null) {
|
|
bindingPropertyName = initializerOutput.metadata.bindingPropertyName;
|
|
} else if (modelMapping !== null) {
|
|
bindingPropertyName = modelMapping.output.bindingPropertyName;
|
|
} else {
|
|
continue;
|
|
}
|
|
|
|
// Validate that initializer-based outputs are not accidentally declared
|
|
// in the `outputs` class metadata.
|
|
if (
|
|
(initializerOutput !== null || modelMapping !== null) &&
|
|
outputsFromMeta.hasOwnProperty(member.name)
|
|
) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.INITIALIZER_API_DECORATOR_METADATA_COLLISION,
|
|
member.node ?? clazz,
|
|
`Output "${member.name}" is unexpectedly declared in @${classDecorator.name} as well.`,
|
|
);
|
|
}
|
|
|
|
outputs[member.name] = bindingPropertyName;
|
|
}
|
|
|
|
return outputs;
|
|
}
|
|
|
|
/** Attempts to parse a decorator-based @Output. */
|
|
function tryParseDecoratorOutput(
|
|
member: ClassMember,
|
|
evaluator: PartialEvaluator,
|
|
isCore: boolean,
|
|
): {decorator: Decorator; metadata: InputOrOutput} | null {
|
|
const decorator = tryGetDecoratorOnMember(member, 'Output', isCore);
|
|
if (decorator === null) {
|
|
return null;
|
|
}
|
|
if (decorator.args !== null && decorator.args.length > 1) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.DECORATOR_ARITY_WRONG,
|
|
decorator.node,
|
|
`@Output can have at most one argument, got ${decorator.args.length} argument(s)`,
|
|
);
|
|
}
|
|
|
|
const classPropertyName = member.name;
|
|
|
|
let alias: string | null = null;
|
|
if (decorator.args?.length === 1) {
|
|
const resolvedAlias = evaluator.evaluate(decorator.args[0]);
|
|
if (typeof resolvedAlias !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
decorator.node,
|
|
resolvedAlias,
|
|
`@Output decorator argument must resolve to a string`,
|
|
);
|
|
}
|
|
alias = resolvedAlias;
|
|
}
|
|
|
|
return {
|
|
decorator,
|
|
metadata: {
|
|
isSignal: false,
|
|
classPropertyName,
|
|
bindingPropertyName: alias ?? classPropertyName,
|
|
},
|
|
};
|
|
}
|
|
|
|
function evaluateHostExpressionBindings(
|
|
hostExpr: ts.Expression,
|
|
evaluator: PartialEvaluator,
|
|
): ParsedHostBindings {
|
|
const hostMetaMap = evaluator.evaluate(hostExpr);
|
|
if (!(hostMetaMap instanceof Map)) {
|
|
throw createValueHasWrongTypeError(
|
|
hostExpr,
|
|
hostMetaMap,
|
|
`Decorator host metadata must be an object`,
|
|
);
|
|
}
|
|
const hostMetadata: Record<string, string | Expression> = {};
|
|
hostMetaMap.forEach((value, key) => {
|
|
// Resolve Enum references to their declared value.
|
|
if (value instanceof EnumValue) {
|
|
value = value.resolved;
|
|
}
|
|
|
|
if (typeof key !== 'string') {
|
|
throw createValueHasWrongTypeError(
|
|
hostExpr,
|
|
key,
|
|
`Decorator host metadata must be a string -> string object, but found unparseable key`,
|
|
);
|
|
}
|
|
|
|
if (typeof value == 'string') {
|
|
hostMetadata[key] = value;
|
|
} else if (value instanceof DynamicValue) {
|
|
hostMetadata[key] = new WrappedNodeExpr(value.node as ts.Expression);
|
|
} else {
|
|
throw createValueHasWrongTypeError(
|
|
hostExpr,
|
|
value,
|
|
`Decorator host metadata must be a string -> string object, but found unparseable value`,
|
|
);
|
|
}
|
|
});
|
|
|
|
const bindings = parseHostBindings(hostMetadata);
|
|
|
|
const errors = verifyHostBindings(bindings, createSourceSpan(hostExpr));
|
|
if (errors.length > 0) {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.HOST_BINDING_PARSE_ERROR,
|
|
getHostBindingErrorNode(errors[0], hostExpr),
|
|
errors.map((error: ParseError) => error.msg).join('\n'),
|
|
);
|
|
}
|
|
|
|
return bindings;
|
|
}
|
|
|
|
/**
|
|
* Attempts to match a parser error to the host binding expression that caused it.
|
|
* @param error Error to match.
|
|
* @param hostExpr Expression declaring the host bindings.
|
|
*/
|
|
function getHostBindingErrorNode(error: ParseError, hostExpr: ts.Expression): ts.Node {
|
|
// In the most common case the `host` object is an object literal with string values. We can
|
|
// confidently match the error to its expression by looking at the string value that the parser
|
|
// failed to parse and the initializers for each of the properties. If we fail to match, we fall
|
|
// back to the old behavior where the error is reported on the entire `host` object.
|
|
if (ts.isObjectLiteralExpression(hostExpr)) {
|
|
for (const prop of hostExpr.properties) {
|
|
if (
|
|
ts.isPropertyAssignment(prop) &&
|
|
ts.isStringLiteralLike(prop.initializer) &&
|
|
error.msg.includes(`[${prop.initializer.text}]`)
|
|
) {
|
|
return prop.initializer;
|
|
}
|
|
}
|
|
}
|
|
|
|
return hostExpr;
|
|
}
|
|
|
|
/**
|
|
* Extracts and prepares the host directives metadata from an array literal expression.
|
|
* @param rawHostDirectives Expression that defined the `hostDirectives`.
|
|
*/
|
|
function extractHostDirectives(
|
|
rawHostDirectives: ts.Expression,
|
|
evaluator: PartialEvaluator,
|
|
reflector: ReflectionHost,
|
|
compilationMode: CompilationMode,
|
|
forwardRefResolver: ForeignFunctionResolver,
|
|
emitDeclarationOnly: boolean,
|
|
): HostDirectiveMeta[] {
|
|
const resolved = evaluator.evaluate(rawHostDirectives, forwardRefResolver);
|
|
if (!Array.isArray(resolved)) {
|
|
throw createValueHasWrongTypeError(
|
|
rawHostDirectives,
|
|
resolved,
|
|
'hostDirectives must be an array',
|
|
);
|
|
}
|
|
|
|
return resolved.map((value) => {
|
|
const hostReference = value instanceof Map ? value.get('directive') : value;
|
|
|
|
// Diagnostics
|
|
if (compilationMode !== CompilationMode.LOCAL) {
|
|
if (!(hostReference instanceof Reference)) {
|
|
throw createValueHasWrongTypeError(
|
|
rawHostDirectives,
|
|
hostReference,
|
|
'Host directive must be a reference',
|
|
);
|
|
}
|
|
|
|
if (!isNamedClassDeclaration(hostReference.node)) {
|
|
throw createValueHasWrongTypeError(
|
|
rawHostDirectives,
|
|
hostReference,
|
|
'Host directive reference must be a class',
|
|
);
|
|
}
|
|
}
|
|
|
|
let directive: Reference<ClassDeclaration> | Expression | ExternalReference;
|
|
let nameForErrors = (fieldName: string) => '@Directive.hostDirectives';
|
|
if (compilationMode === CompilationMode.LOCAL && hostReference instanceof DynamicValue) {
|
|
// At the moment in local compilation we only support simple array for host directives, i.e.,
|
|
// an array consisting of the directive identifiers. We don't support forward refs or other
|
|
// expressions applied on externally imported directives. The main reason is simplicity, and
|
|
// that almost nobody wants to use host directives this way (e.g., what would be the point of
|
|
// forward ref for imported symbols?!)
|
|
if (
|
|
!ts.isIdentifier(hostReference.node) &&
|
|
!ts.isPropertyAccessExpression(hostReference.node)
|
|
) {
|
|
const compilationModeName = emitDeclarationOnly
|
|
? 'experimental declaration-only emission'
|
|
: 'local compilation';
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
|
|
hostReference.node,
|
|
`In ${compilationModeName} mode, host directive cannot be an expression. Use an identifier instead`,
|
|
);
|
|
}
|
|
|
|
if (emitDeclarationOnly) {
|
|
if (ts.isIdentifier(hostReference.node)) {
|
|
const importInfo = reflector.getImportOfIdentifier(hostReference.node);
|
|
if (importInfo) {
|
|
directive = new ExternalReference(importInfo.from, importInfo.name);
|
|
} else {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
|
|
hostReference.node,
|
|
`In experimental declaration-only emission mode, host directive cannot use indirect external indentifiers. Use a direct external identifier instead`,
|
|
);
|
|
}
|
|
} else {
|
|
throw new FatalDiagnosticError(
|
|
ErrorCode.LOCAL_COMPILATION_UNSUPPORTED_EXPRESSION,
|
|
hostReference.node,
|
|
`In experimental declaration-only emission mode, host directive cannot be an expression. Use an identifier instead`,
|
|
);
|
|
}
|
|
} else {
|
|
directive = new WrappedNodeExpr(hostReference.node);
|
|
}
|
|
} else if (hostReference instanceof Reference) {
|
|
directive = hostReference as Reference<ClassDeclaration>;
|
|
nameForErrors = (fieldName: string) =>
|
|
`@Directive.hostDirectives.${
|
|
(directive as Reference<ClassDeclaration>).node.name.text
|
|
}.${fieldName}`;
|
|
} else {
|
|
throw new Error('Impossible state');
|
|
}
|
|
|
|
const meta: HostDirectiveMeta = {
|
|
directive,
|
|
isForwardReference: hostReference instanceof Reference && hostReference.synthetic,
|
|
inputs: parseHostDirectivesMapping(
|
|
'inputs',
|
|
value,
|
|
nameForErrors('input'),
|
|
rawHostDirectives,
|
|
),
|
|
outputs: parseHostDirectivesMapping(
|
|
'outputs',
|
|
value,
|
|
nameForErrors('output'),
|
|
rawHostDirectives,
|
|
),
|
|
};
|
|
|
|
return meta;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Parses the expression that defines the `inputs` or `outputs` of a host directive.
|
|
* @param field Name of the field that is being parsed.
|
|
* @param resolvedValue Evaluated value of the expression that defined the field.
|
|
* @param classReference Reference to the host directive class.
|
|
* @param sourceExpression Expression that the host directive is referenced in.
|
|
*/
|
|
function parseHostDirectivesMapping(
|
|
field: 'inputs' | 'outputs',
|
|
resolvedValue: ResolvedValue,
|
|
nameForErrors: string,
|
|
sourceExpression: ts.Expression,
|
|
): {[bindingPropertyName: string]: string} | null {
|
|
if (resolvedValue instanceof Map && resolvedValue.has(field)) {
|
|
const rawInputs = resolvedValue.get(field);
|
|
|
|
if (isStringArrayOrDie(rawInputs, nameForErrors, sourceExpression)) {
|
|
return parseMappingStringArray(rawInputs);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** Converts the parsed host directive information into metadata. */
|
|
function toHostDirectiveMetadata(
|
|
hostDirective: HostDirectiveMeta,
|
|
context: ts.SourceFile,
|
|
refEmitter: ReferenceEmitter,
|
|
): R3HostDirectiveMetadata {
|
|
let directive: R3Reference;
|
|
if (hostDirective.directive instanceof Reference) {
|
|
directive = toR3Reference(
|
|
hostDirective.directive.node,
|
|
hostDirective.directive,
|
|
context,
|
|
refEmitter,
|
|
);
|
|
} else if (hostDirective.directive instanceof ExternalReference) {
|
|
directive = {
|
|
value: new ExternalExpr(hostDirective.directive),
|
|
type: new ExternalExpr(hostDirective.directive),
|
|
};
|
|
} else {
|
|
directive = {
|
|
value: hostDirective.directive,
|
|
type: hostDirective.directive,
|
|
};
|
|
}
|
|
|
|
return {
|
|
directive,
|
|
isForwardReference: hostDirective.isForwardReference,
|
|
inputs: hostDirective.inputs || null,
|
|
outputs: hostDirective.outputs || null,
|
|
};
|
|
}
|
|
|
|
/** Converts the parsed input information into metadata. */
|
|
function toR3InputMetadata(mapping: InputMapping): R3InputMetadata {
|
|
return {
|
|
classPropertyName: mapping.classPropertyName,
|
|
bindingPropertyName: mapping.bindingPropertyName,
|
|
required: mapping.required,
|
|
transformFunction:
|
|
mapping.transform !== null ? new WrappedNodeExpr(mapping.transform.node) : null,
|
|
isSignal: mapping.isSignal,
|
|
};
|
|
}
|
|
|
|
export function extractHostBindingResources(nodes: HostBindingNodes): ReadonlySet<Resource> {
|
|
const result = new Set<Resource>();
|
|
|
|
if (nodes.literal !== null) {
|
|
result.add({path: null, node: nodes.literal});
|
|
}
|
|
|
|
for (const current of nodes.bindingDecorators) {
|
|
result.add({path: null, node: current.expression});
|
|
}
|
|
|
|
for (const current of nodes.listenerDecorators) {
|
|
result.add({path: null, node: current.expression});
|
|
}
|
|
|
|
return result;
|
|
}
|