diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts index e6d782d461cd..98903c398ef6 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts @@ -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, @@ -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 @@ -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; @@ -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(); + 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. @@ -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 { @@ -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); } /** diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts index 5814cf434d80..e117844ca449 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/test/metadata_spec.ts @@ -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 }] })`, ); }); diff --git a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts index b3d350493e7d..7a7f4378d1ec 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts @@ -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'; @@ -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. @@ -299,6 +306,7 @@ export class ComponentDecoratorHandler private literalCache = new Map(); private elementSchemaRegistry = new DomElementSchemaRegistry(); + private readonly undecoratedMetadataExtractor: UndecoratedMetadataExtractor; /** * During the asynchronous preanalyze phase, it's necessary to parse the template to extract @@ -971,6 +979,7 @@ export class ComponentDecoratorHandler this.isCore, this.annotateForClosureCompiler, (dec) => transformDecoratorResources(dec, component, styles, template), + this.undecoratedMetadataExtractor, ) : null, classDebugInfo: extractClassDebugInfo( diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts index c97a5c8f9879..875308dadc03 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/handler.ts @@ -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 { @@ -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, @@ -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), diff --git a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts index 65830fd6bb42..f5ecd7265b3e 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/directive/src/shared.ts @@ -14,6 +14,8 @@ import { ExternalReference, ForwardRefHandling, getSafePropertyAccessString, + LiteralArrayExpr, + literalMap, MaybeForwardRefExpression, ParsedHostBindings, ParseError, @@ -24,7 +26,10 @@ import { R3QueryMetadata, R3Reference, verifyHostBindings, + R3Identifiers, + ArrowFunctionExpr, WrappedNodeExpr, + literal, } from '@angular/compiler'; import ts from 'typescript'; @@ -76,6 +81,7 @@ import { ReferencesRegistry, toR3Reference, tryUnwrapForwardRef, + UndecoratedMetadataExtractor, unwrapConstructorDependencies, unwrapExpression, validateConstructorDependencies, @@ -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( + 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; diff --git a/packages/compiler-cli/test/compliance/test_cases/model_inputs/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/model_inputs/GOLDEN_PARTIAL.js index 9dc3dd4539ec..33d14ee9b3b6 100644 --- a/packages/compiler-cli/test/compliance/test_cases/model_inputs/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/model_inputs/GOLDEN_PARTIAL.js @@ -14,7 +14,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }, { type: i0.Output, args: ["nameChange"] }] } }); /**************************************************************************************************** * PARTIAL FILE: model_directive_definition.d.ts @@ -45,7 +45,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ template: 'Works', }] - }] }); + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }, { type: i0.Output, args: ["nameChange"] }] } }); /**************************************************************************************************** * PARTIAL FILE: model_component_definition.d.ts @@ -80,7 +80,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }], propDecorators: { decoratorInput: [{ + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }, { type: i0.Output, args: ["counterChange"] }], modelWithAlias: [{ type: i0.Input, args: [{ isSignal: true, alias: "alias", required: false }] }, { type: i0.Output, args: ["aliasChange"] }], decoratorInput: [{ type: Input }], decoratorInputWithAlias: [{ type: Input, diff --git a/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js index bfb7ec07b0cf..724a4967fce1 100644 --- a/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/output_function/GOLDEN_PARTIAL.js @@ -18,7 +18,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { a: [{ type: i0.Output, args: ["a"] }], b: [{ type: i0.Output, args: ["b"] }], c: [{ type: i0.Output, args: ["cPublic"] }], d: [{ type: i0.Output, args: ["d"] }], e: [{ type: i0.Output, args: ["e"] }] } }); /**************************************************************************************************** * PARTIAL FILE: output_in_directive.d.ts @@ -56,7 +56,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ template: 'Works', }] - }] }); + }], propDecorators: { a: [{ type: i0.Output, args: ["a"] }], b: [{ type: i0.Output, args: ["b"] }], c: [{ type: i0.Output, args: ["cPublic"] }], d: [{ type: i0.Output, args: ["d"] }], e: [{ type: i0.Output, args: ["e"] }] } }); /**************************************************************************************************** * PARTIAL FILE: output_in_component.d.ts @@ -97,7 +97,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ standalone: true, }] - }], propDecorators: { clickDecorator1: [{ + }], propDecorators: { click1: [{ type: i0.Output, args: ["click1"] }], click2: [{ type: i0.Output, args: ["click2"] }], click3: [{ type: i0.Output, args: ["click3"] }], _bla: [{ type: i0.Output, args: ["decoratorPublicName"] }], _bla2: [{ type: i0.Output, args: ["decoratorPublicName2"] }], clickDecorator1: [{ type: Output }], clickDecorator2: [{ type: Output diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js index 2831f0d23ba7..b4805ea15ef0 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_bindings/host_bindings/GOLDEN_PARTIAL.js @@ -833,13 +833,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE selector: '[hostBindingDir]', standalone: false }] - }], propDecorators: { 'is-a': [{ + }], propDecorators: { "is-a": [{ type: HostBinding, args: ['class.a'] - }], 'is-"b"': [{ + }], "is-\"b\"": [{ type: HostBinding, args: ['class.b'] - }], '"is-c"': [{ + }], "\"is-c\"": [{ type: HostBinding, args: ['class.c'] }] } }); diff --git a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js index 166e20b604ae..4b1957b46610 100644 --- a/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/r3_view_compiler_listener/GOLDEN_PARTIAL.js @@ -977,7 +977,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ selector: '[ngModel]', }] - }] }); + }], propDecorators: { ngModel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngModel", required: true }] }, { type: i0.Output, args: ["ngModelChange"] }] } }); export class TestCmp { constructor() { this.names = [signal('Angular')]; @@ -1031,7 +1031,7 @@ NgModelDirective.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", versi i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: NgModelDirective, decorators: [{ type: Directive, args: [{ selector: '[ngModel]' }] - }] }); + }], propDecorators: { ngModel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ngModel", required: false }] }, { type: i0.Output, args: ["ngModelChange"] }] } }); export class TestCmp { constructor() { this.value = 123; diff --git a/packages/compiler-cli/test/compliance/test_cases/signal_inputs/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/signal_inputs/GOLDEN_PARTIAL.js index c403b60b196c..57031f20c3f4 100644 --- a/packages/compiler-cli/test/compliance/test_cases/signal_inputs/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/signal_inputs/GOLDEN_PARTIAL.js @@ -14,7 +14,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: input_directive_definition.d.ts @@ -45,7 +45,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ template: 'Works', }] - }] }); + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: input_component_definition.d.ts @@ -81,7 +81,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }], propDecorators: { decoratorInput: [{ + }], propDecorators: { counter: [{ type: i0.Input, args: [{ isSignal: true, alias: "counter", required: false }] }], signalWithTransform: [{ type: i0.Input, args: [{ isSignal: true, alias: "signalWithTransform", required: false }] }], signalWithTransformAndAlias: [{ type: i0.Input, args: [{ isSignal: true, alias: "publicNameSignal", required: false }] }], decoratorInput: [{ type: Input }], decoratorInputWithAlias: [{ type: Input, @@ -127,7 +127,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: transform_not_captured.d.ts @@ -163,7 +163,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], name2: [{ type: i0.Input, args: [{ isSignal: true, alias: "name2", required: true }] }], genericTransform: [{ type: i0.Input, args: [{ isSignal: true, alias: "genericTransform", required: true }] }], genericTransform2: [{ type: i0.Input, args: [{ isSignal: true, alias: "genericTransform2", required: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: complex_transform_functions.d.ts diff --git a/packages/compiler-cli/test/compliance/test_cases/signal_queries/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/signal_queries/GOLDEN_PARTIAL.js index 33d4b3d20ff3..3229468d1dba 100644 --- a/packages/compiler-cli/test/compliance/test_cases/signal_queries/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/signal_queries/GOLDEN_PARTIAL.js @@ -24,7 +24,7 @@ TestDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "0.0. i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: TestDir, decorators: [{ type: Directive, args: [{}] - }] }); + }], propDecorators: { query1: [{ type: i0.ViewChild, args: ['locatorA', { isSignal: true }] }], query2: [{ type: i0.ViewChildren, args: ['locatorB', { isSignal: true }] }], query3: [{ type: i0.ContentChild, args: ['locatorC', { isSignal: true }] }], query4: [{ type: i0.ContentChildren, args: ['locatorD', { isSignal: true }] }], query5: [{ type: i0.ViewChild, args: [forwardRef(() => SomeToken), { isSignal: true }] }], query6: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => SomeToken), { isSignal: true }] }], query7: [{ type: i0.ViewChild, args: ['locatorE', Object.assign({ read: SomeToken }, { isSignal: true })] }], query8: [{ type: i0.ContentChildren, args: ['locatorF, locatorG', Object.assign({ descendants: true }, { isSignal: true })] }], query9: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => nonAnalyzableRefersToString), Object.assign({ descendants: true }, { isSignal: true })] }] } }); /**************************************************************************************************** * PARTIAL FILE: query_in_directive.d.ts @@ -66,7 +66,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE args: [{ template: 'Works', }] - }] }); + }], propDecorators: { query1: [{ type: i0.ViewChild, args: ['locatorA', { isSignal: true }] }], query2: [{ type: i0.ViewChildren, args: ['locatorB', { isSignal: true }] }], query3: [{ type: i0.ContentChild, args: ['locatorC', { isSignal: true }] }], query4: [{ type: i0.ContentChildren, args: ['locatorD', { isSignal: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: query_in_component.d.ts @@ -100,10 +100,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE }], propDecorators: { decoratorViewChild: [{ type: ViewChild, args: ['locator1'] - }], decoratorContentChild: [{ + }], signalViewChild: [{ type: i0.ViewChild, args: ['locator1', { isSignal: true }] }], decoratorContentChild: [{ type: ContentChild, args: ['locator2'] - }] } }); + }], signalContentChild: [{ type: i0.ContentChild, args: ['locator2', { isSignal: true }] }] } }); /**************************************************************************************************** * PARTIAL FILE: mixed_query_variants.d.ts diff --git a/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/GOLDEN_PARTIAL.js b/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/GOLDEN_PARTIAL.js index 37f60fb068ca..4dc29dd6c109 100644 --- a/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/GOLDEN_PARTIAL.js +++ b/packages/compiler-cli/test/compliance/test_cases/source_mapping/inline_templates/GOLDEN_PARTIAL.js @@ -930,7 +930,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE /**************************************************************************************************** * PARTIAL FILE: two_way_binding_simple.js.map ****************************************************************************************************/ -{"version":3,"file":"two_way_binding_simple.js","sourceRoot":"","sources":["../two_way_binding_simple.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,kCAAkC,gFAWnC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,kCAAkC;oBAC5C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;8BAEU,OAAO;sBAAf,KAAK;gBACI,cAAc;sBAAvB,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} +{"version":3,"file":"two_way_binding_simple.js","sourceRoot":"","sources":["../two_way_binding_simple.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,kCAAkC,gFAWnC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,kCAAkC;oBAC5C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;;sBAEE,KAAK;;sBACL,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} /**************************************************************************************************** * PARTIAL FILE: two_way_binding_simple.d.ts ****************************************************************************************************/ @@ -1005,7 +1005,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE /**************************************************************************************************** * PARTIAL FILE: two_way_binding_simple.js.map ****************************************************************************************************/ -{"version":3,"file":"two_way_binding_simple.js","sourceRoot":"","sources":["../two_way_binding_simple.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,kCAAkC,gFAWnC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,kCAAkC;oBAC5C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;8BAEU,OAAO;sBAAf,KAAK;gBACI,cAAc;sBAAvB,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} +{"version":3,"file":"two_way_binding_simple.js","sourceRoot":"","sources":["../two_way_binding_simple.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,kCAAkC,gFAWnC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,kCAAkC;oBAC5C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;;sBAEE,KAAK;;sBACL,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} /**************************************************************************************************** * PARTIAL FILE: two_way_binding_simple.d.ts ****************************************************************************************************/ @@ -1080,7 +1080,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE /**************************************************************************************************** * PARTIAL FILE: two_way_binding_longhand.js.map ****************************************************************************************************/ -{"version":3,"file":"two_way_binding_longhand.js","sourceRoot":"","sources":["../two_way_binding_longhand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,qCAAqC,gFAWtC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,qCAAqC;oBAC/C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;8BAEU,OAAO;sBAAf,KAAK;gBACI,cAAc;sBAAvB,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} +{"version":3,"file":"two_way_binding_longhand.js","sourceRoot":"","sources":["../two_way_binding_longhand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,qCAAqC,gFAWtC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,qCAAqC;oBAC/C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;;sBAEE,KAAK;;sBACL,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} /**************************************************************************************************** * PARTIAL FILE: two_way_binding_longhand.d.ts ****************************************************************************************************/ @@ -1155,7 +1155,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE /**************************************************************************************************** * PARTIAL FILE: two_way_binding_longhand.js.map ****************************************************************************************************/ -{"version":3,"file":"two_way_binding_longhand.js","sourceRoot":"","sources":["../two_way_binding_longhand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,qCAAqC,gFAWtC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,qCAAqC;oBAC/C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;8BAEU,OAAO;sBAAf,KAAK;gBACI,cAAc;sBAAvB,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} +{"version":3,"file":"two_way_binding_longhand.js","sourceRoot":"","sources":["../two_way_binding_longhand.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAC,MAAM,eAAe,CAAC;;AAO1F,MAAM,OAAO,OAAO;IALpB;QAME,SAAI,GAAW,EAAE,CAAC;KACnB;;+GAFY,OAAO;mGAAP,OAAO,qEAHN,qCAAqC,gFAWtC,gBAAgB;sGARhB,OAAO;kBALnB,SAAS;mBAAC;oBACP,QAAQ,EAAE,UAAU;oBACpB,QAAQ,EAAE,qCAAqC;oBAC/C,UAAU,EAAE,KAAK;iBACpB;;AASD,MAAM,OAAO,gBAAgB;IAJ7B;QAKW,YAAO,GAAW,EAAE,CAAC;QACpB,mBAAc,GAAyB,IAAI,YAAY,EAAE,CAAC;KACrE;;wHAHY,gBAAgB;4GAAhB,gBAAgB;sGAAhB,gBAAgB;kBAJ5B,SAAS;mBAAC;oBACP,QAAQ,EAAE,WAAW;oBACrB,UAAU,EAAE,KAAK;iBACpB;;sBAEE,KAAK;;sBACL,MAAM;;AAIT,MAAM,OAAO,SAAS;;iHAAT,SAAS;kHAAT,SAAS,iBAdT,OAAO,EAQP,gBAAgB;kHAMhB,SAAS;sGAAT,SAAS;kBADrB,QAAQ;mBAAC,EAAC,YAAY,EAAE,CAAC,OAAO,EAAE,gBAAgB,CAAC,EAAC"} /**************************************************************************************************** * PARTIAL FILE: two_way_binding_longhand.d.ts ****************************************************************************************************/ diff --git a/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts index 34831b88d8b6..379a3130a75a 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_inputs_spec.ts @@ -467,5 +467,25 @@ runInEachFileSystem(() => { const js = env.getContents('test.js'); expect(js).toContain('inputs: { data: [1, "data"] }'); }); + + it('should capture signal inputs in the setClassMetadata call', () => { + env.write( + 'test.ts', + ` + import {Directive, input} from '@angular/core'; + + @Directive() + export class TestDir { + data = input('test'); + } + `, + ); + env.driveMain(); + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Directive + }], null, { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: false }] }] });`); + }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts index 13fe7023145a..f36d1d91bbc2 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_models_spec.ts @@ -776,5 +776,30 @@ runInEachFileSystem(() => { expect(diagnostics.length).toBe(0); }); }); + + it('should capture model input/output pair in the setClassMetadata call', () => { + env.write( + 'test.ts', + ` + import {Directive, model} from '@angular/core'; + + @Directive() + export class TestDir { + value = model(1); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain( + `i0.ɵsetClassMetadata(TestDir, [{ + type: Directive + }], null, { value: [` + + `{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, ` + + `{ type: i0.Output, args: ["valueChange"] }] });`, + ); + }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts index b3eb034ea66d..48a3dad4dad5 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_outputs_spec.ts @@ -324,5 +324,26 @@ runInEachFileSystem(() => { expect(diagnostics.length).toBe(0); }); }); + + it('should capture initializer-based outputs in the setClassMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, output} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + click = output(); + } + `, + ); + env.driveMain(); + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { click: [{ type: i0.Output, args: ["click"] }] });`); + }); }); }); diff --git a/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts b/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts index 6a9ca68e80af..1835642d7730 100644 --- a/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts +++ b/packages/compiler-cli/test/ngtsc/authoring_queries_spec.ts @@ -389,6 +389,146 @@ runInEachFileSystem(() => { const diagnostics = env.driveDiagnostics(); expect(diagnostics.length).toBe(0); }); + + it('should capture a viewChild query in the setClasMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, viewChild} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = viewChild('myLocator'); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ViewChild, args: ['myLocator', { isSignal: true }] }] });`); + }); + + it('should capture a viewChildren query in the setClasMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, viewChildren} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = viewChildren('myLocator'); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ViewChildren, args: ['myLocator', { isSignal: true }] }] });`); + }); + + it('should capture a contentChild query in the setClasMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, contentChild} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = contentChild('myLocator'); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ContentChild, args: ['myLocator', { isSignal: true }] }] });`); + }); + + it('should capture a contentChildren query in the setClasMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, contentChildren} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = contentChildren('myLocator'); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain(`i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ContentChildren, args: ['myLocator', { isSignal: true }] }] });`); + }); + + it('should capture a query with options in a setClassMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, viewChild, ElementRef} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = viewChild('myLocator', {read: ElementRef}); + } + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain( + `i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ViewChild, ` + + `args: ['myLocator', Object.assign({ read: ElementRef }, { isSignal: true })] }] });`, + ); + }); + + it('should wrap reference in query as a forwardRef in the setClassMetadata call', () => { + env.write( + 'test.ts', + ` + import {Component, Directive, viewChild} from '@angular/core'; + + @Component({selector: 'test', template: ''}) + export class TestDir { + el = viewChild(Dep); + } + + @Directive({selector: '[dep]'}) + export class Dep {} + `, + ); + env.driveMain(); + + const js = env.getContents('test.js'); + expect(js).toContain('import * as i0 from "@angular/core";'); + expect(js).toContain( + `i0.ɵsetClassMetadata(TestDir, [{ + type: Component, + args: [{ selector: 'test', template: '' }] + }], null, { el: [{ type: i0.ViewChild, args: [i0.forwardRef(() => Dep), { isSignal: true }] }] });`, + ); + }); }); }); }); diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 3c48d64de48d..c2cf7044785c 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -477,6 +477,17 @@ export class Identifiers { moduleName: CORE, }; + // Decorators + static inputDecorator: o.ExternalReference = {name: 'Input', moduleName: CORE}; + static outputDecorator: o.ExternalReference = {name: 'Output', moduleName: CORE}; + static viewChildDecorator: o.ExternalReference = {name: 'ViewChild', moduleName: CORE}; + static viewChildrenDecorator: o.ExternalReference = {name: 'ViewChildren', moduleName: CORE}; + static contentChildDecorator: o.ExternalReference = {name: 'ContentChild', moduleName: CORE}; + static contentChildrenDecorator: o.ExternalReference = { + name: 'ContentChildren', + moduleName: CORE, + }; + // type-checking static InputSignalBrandWriteType = {name: 'ɵINPUT_SIGNAL_BRAND_WRITE_TYPE', moduleName: CORE}; static UnwrapDirectiveSignalInputs = {name: 'ɵUnwrapDirectiveSignalInputs', moduleName: CORE}; diff --git a/packages/core/test/render3/jit_environment_spec.ts b/packages/core/test/render3/jit_environment_spec.ts index 87db419d252c..1184eaa4920b 100644 --- a/packages/core/test/render3/jit_environment_spec.ts +++ b/packages/core/test/render3/jit_environment_spec.ts @@ -29,6 +29,12 @@ const INTERFACE_EXCEPTIONS = new Set([ const AOT_ONLY = new Set([ 'ɵsetClassMetadata', 'ɵsetClassMetadataAsync', + 'Input', + 'Output', + 'ViewChild', + 'ViewChildren', + 'ContentChild', + 'ContentChildren', // used in type-checking. 'ɵINPUT_SIGNAL_BRAND_WRITE_TYPE',