From bbc970bb0b31b02130514f1f0cb5ba5939b9300a Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 13 Aug 2024 11:26:00 +0000 Subject: [PATCH] refactor(migrations): always add `readonly` to migrated signal inputs (#57368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signal inputs are no longer updated by assignment, unlike `@Input()`, so a good practice is adding `readonly` for the `InputSignal`— which should never be swapped out. This is a safe operation because the migration skips all inputs that are being written anyway. PR Close #57368 --- .../src/convert-input/convert_to_signal.ts | 13 +- .../src/convert-input/prepare_and_check.ts | 10 ++ .../src/input_detection/input_decorator.ts | 5 +- .../test/golden-test/modifier_tests.ts | 15 ++ .../signal-migration/test/golden.txt | 110 ++++++++------ .../test/golden_best_effort.txt | 138 ++++++++++-------- 6 files changed, 180 insertions(+), 111 deletions(-) create mode 100644 packages/core/schematics/migrations/signal-migration/test/golden-test/modifier_tests.ts diff --git a/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts b/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts index c298b06d3f1..c8da41cea11 100644 --- a/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts +++ b/packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts @@ -32,6 +32,7 @@ export function convertToSignalInput( resolvedType, preferShorthandIfPossible, isUndefinedInitialValue, + originalInputDecorator, }: ConvertInputPreparation, checker: ts.TypeChecker, importManager: ImportManager, @@ -117,12 +118,16 @@ export function convertToSignalInput( inputArgs, ); - // TODO: - // - modifiers (but private does not work) - // - preserve custom decorators etc. + let modifiersWithoutInputDecorator = + node.modifiers?.filter((m) => m !== originalInputDecorator.node) ?? []; + + // Add `readonly` to all new signal input declarations. + if (!modifiersWithoutInputDecorator?.some((s) => s.kind === ts.SyntaxKind.ReadonlyKeyword)) { + modifiersWithoutInputDecorator.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); + } const result = ts.factory.createPropertyDeclaration( - undefined, + modifiersWithoutInputDecorator, node.name, undefined, undefined, diff --git a/packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts b/packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts index cdc5fc1aa5d..e8371f1e3fa 100644 --- a/packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts +++ b/packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts @@ -13,6 +13,8 @@ import { InputMemberIncompatibility, } from '../input_detection/incompatibility'; import {InputNode} from '../input_detection/input_node'; +import {Decorator} from '../../../../../../compiler-cli/src/ngtsc/reflection'; +import assert from 'assert'; /** * Interface describing analysis performed when the input @@ -23,6 +25,7 @@ export interface ConvertInputPreparation { preferShorthandIfPossible: {originalType: ts.TypeNode} | null; isUndefinedInitialValue: boolean; resolvedMetadata: ExtractedInput; + originalInputDecorator: Decorator; } /** @@ -46,6 +49,12 @@ export function prepareAndCheckForConversion( reason: InputIncompatibilityReason.Accessor, }; } + + assert( + metadata.inputDecorator !== null, + 'Expected an input decorator for inputs that are being migrated.', + ); + const initialValue = node.initializer; // If an input can be required, due to the non-null assertion on the property, @@ -118,5 +127,6 @@ export function prepareAndCheckForConversion( resolvedType: typeToAdd, preferShorthandIfPossible, isUndefinedInitialValue, + originalInputDecorator: metadata.inputDecorator, }; } diff --git a/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts b/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts index 53ef015f0d6..f74ad6c5c4c 100644 --- a/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts +++ b/packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts @@ -24,7 +24,7 @@ import { } from '../../../../../../compiler-cli/src/ngtsc/partial_evaluator'; import { ClassDeclaration, - DecoratorIdentifier, + Decorator, ReflectionHost, } from '../../../../../../compiler-cli/src/ngtsc/reflection'; import {CompilationMode} from '../../../../../../compiler-cli/src/ngtsc/transform'; @@ -34,6 +34,7 @@ import {InputNode, isInputContainerNode} from '../input_detection/input_node'; /** Metadata extracted of an input declaration (in `.ts` or `.d.ts` files). */ export interface ExtractedInput extends InputMapping { inSourceFile: boolean; + inputDecorator: Decorator | null; } /** Attempts to extract metadata of a potential TypeScript `@Input()` declaration. */ @@ -86,6 +87,7 @@ function extractDtsInput(node: ts.Node, metadataReader: DtsMetadataReader): Extr ? null : { ...inputMapping, + inputDecorator: null, inSourceFile: false, }; } @@ -150,6 +152,7 @@ function extractSourceCodeInput( isSignal: false, inSourceFile: true, transform: transformResult, + inputDecorator, }; } diff --git a/packages/core/schematics/migrations/signal-migration/test/golden-test/modifier_tests.ts b/packages/core/schematics/migrations/signal-migration/test/golden-test/modifier_tests.ts new file mode 100644 index 00000000000..5c4c74bc007 --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/test/golden-test/modifier_tests.ts @@ -0,0 +1,15 @@ +// tslint:disable + +import {Component, Input} from '@angular/core'; + +function CustomDecorator() { + return (a: any, b: any) => {}; +} + +@Component({template: ''}) +class ModifierScenarios { + @Input() readonly alreadyReadonly = true; + @Input() protected ImProtected = true; + @Input() protected readonly ImProtectedAndReadonly = true; + @Input() @CustomDecorator() protected readonly usingCustomDecorator = true; +} diff --git a/packages/core/schematics/migrations/signal-migration/test/golden.txt b/packages/core/schematics/migrations/signal-migration/test/golden.txt index 20d556d9e60..664e6f3bbd6 100644 --- a/packages/core/schematics/migrations/signal-migration/test/golden.txt +++ b/packages/core/schematics/migrations/signal-migration/test/golden.txt @@ -9,11 +9,11 @@ import {By} from '@angular/platform-browser'; function it(msg: string, fn: () => void) {} class SubDir { - name = input('John'); + readonly name = input('John'); } class MyComp { - hello = input(''); + readonly hello = input(''); } it('should work', () => { @@ -40,12 +40,12 @@ class Sub implements BaseNonAngular { } class BaseWithAngular { - disabled = input(''); + readonly disabled = input(''); } @Directive() class Sub2 extends BaseWithAngular { - disabled = input(''); + readonly disabled = input(''); } interface BaseNonAngularInterface { @@ -67,7 +67,7 @@ class BothInputImported { @Input() decoratorInput = true; signalInput = input(); - thisCanBeMigrated = input(true); + readonly thisCanBeMigrated = input(true); __makeDecoratorInputNonMigratable() { this.decoratorInput = false; @@ -87,7 +87,7 @@ function it(msg: string, fn: () => void) {} function bootstrapTemplate(tmpl: string, inputs: unknown) {} class MyComp { - hello = input(''); + readonly hello = input(''); } it('should work', () => { @@ -109,7 +109,7 @@ import { Component, input } from '@angular/core'; `, }) class Group { - label = input.required(); + readonly label = input.required(); } @Component({ @@ -182,7 +182,7 @@ let nextUniqueId = 0; @Directive() export class MatHint { align: string = ''; - id = input(`mat-hint-${nextUniqueId++}`); + readonly id = input(`mat-hint-${nextUniqueId++}`); } @Component({ @@ -226,7 +226,7 @@ import { input } from '@angular/core'; class ExistingSignalImport { signalInput = input(); - thisCanBeMigrated = input(true); + readonly thisCanBeMigrated = input(true); } @@@@@@ getters.ts @@@@@@ @@ -266,7 +266,7 @@ import { Component, input } from '@angular/core'; }, }) class HostBindingTestCmp { - id = input('works'); + readonly id = input('works'); // for testing nested expressions. nested = this; @@ -284,7 +284,7 @@ const SHARED = { host: SHARED, }) class HostBindingsShared { - id = input(false); + readonly id = input(false); } @Component({ @@ -292,8 +292,8 @@ class HostBindingsShared { host: SHARED, }) class HostBindingsShared2 { - id = input(false); - id2 = input(false); + readonly id = input(false); + readonly id2 = input(false); } @@@@@@ identifier_collisions.ts @@@@@@ @@ -313,8 +313,8 @@ const complex = 'some global variable'; @Component({template: ''}) class MyComp { - name = input(null); - complex = input(null); + readonly name = input(null); + readonly complex = input(null); valid() { const name = this.name(); @@ -371,10 +371,10 @@ interface Audi extends Car { templateUrl: './template.html', }) export class AppComponent { - input = input(null); - bla = input.required({ transform: disabledTransform }); - narrowableMultipleTimes = input(null); - withUndefinedInput = input(); + readonly input = input(null); + readonly bla = input.required({ transform: disabledTransform }); + readonly narrowableMultipleTimes = input(null); + readonly withUndefinedInput = input(); @Input() incompatible: string | null = null; private _bla: any; @@ -496,7 +496,7 @@ import { Component, input } from '@angular/core'; @Component({template: ''}) class IndexAccessInput { - items = input([]); + readonly items = input([]); bla() { const {items} = this; @@ -558,7 +558,7 @@ import { Component, input } from '@angular/core'; `, }) export class InlineTmpl { - justify = input<'start' | 'end'>('end'); + readonly justify = input<'start' | 'end'>('end'); } @@@@@@ jit_true_component_external_tmpl.html @@@@@@ @@ -582,7 +582,7 @@ import { Component, input } from '@angular/core'; template: '{{test()}}', }) class JitTrueComponent { - test = input(true); + readonly test = input(true); } @Component({ @@ -590,7 +590,7 @@ class JitTrueComponent { templateUrl: './jit_true_component_external_tmpl.html', }) class JitTrueComponentExternalTmpl { - test = input(true); + readonly test = input(true); } @@@@@@ loop_labels.ts @@@@@@ @@ -599,7 +599,7 @@ class JitTrueComponentExternalTmpl { import { input } from '@angular/core'; class MyTestCmp { - someInput = input.required(); + readonly someInput = input.required(); tmpValue = false; @@ -638,6 +638,24 @@ import {Component, Input} from '@angular/core'; export class ManualInstantiation { @Input() bla: string = ''; } +@@@@@@ modifier_tests.ts @@@@@@ + +// tslint:disable + +import { Component, input } from '@angular/core'; + +function CustomDecorator() { + return (a: any, b: any) => {}; +} + +@Component({template: ''}) +class ModifierScenarios { + readonly alreadyReadonly = input(true); + protected readonly ImProtected = input(true); + protected readonly ImProtectedAndReadonly = input(true); + @CustomDecorator() +protected readonly usingCustomDecorator = input(true); +} @@@@@@ mutate.ts @@@@@@ // tslint:disable @@ -645,7 +663,7 @@ export class ManualInstantiation { import { input } from '@angular/core'; export class TestCmp { - shared = input<{ + readonly shared = input<{ x: string; }>({ x: '' }); @@ -678,7 +696,7 @@ interface Config { `, }) export class NestedTemplatePropAccess { - config = input({}); + readonly config = input({}); } @@@@@@ object_expansion.ts @@@@@@ @@ -688,7 +706,7 @@ import { Component, input } from '@angular/core'; @Component({}) export class ObjectExpansion { - bla = input(''); + readonly bla = input(''); expansion() { const {bla} = this; @@ -720,7 +738,7 @@ import { Directive, input } from '@angular/core'; @Directive() class OptionalInput { - bla = input(); + readonly bla = input(); } @@@@@@ problematic_type_reference.ts @@@@@@ @@ -767,12 +785,12 @@ import {COMPLEX_VAR} from './required-no-explicit-type-extra'; export const CONST = {field: true}; export class RequiredNoExplicitType { - someInputNumber = input.required(); - someInput = input.required(); - withConstInitialVal = input.required(); + readonly someInputNumber = input.required(); + readonly someInput = input.required(); + readonly withConstInitialVal = input.required(); // typing this explicitly now would require same imports as from the `-extra` file. - complexVal = input.required(); + readonly complexVal = input.required(); } @@@@@@ required.ts @@@@@@ @@ -781,7 +799,7 @@ export class RequiredNoExplicitType { import { input } from '@angular/core'; class Required { - simpleInput = input.required(); + readonly simpleInput = input.required(); } @@@@@@ safe_property_reads.ts @@@@@@ @@ -803,7 +821,7 @@ import { Component, input } from '@angular/core'; `, }) class WithSafePropertyReads { - myInput = input(0); + readonly myInput = input(0); bla: this | undefined = this; } @@ -814,7 +832,7 @@ class WithSafePropertyReads { import { input } from '@angular/core'; export class TestCmp { - shared = input(false); + readonly shared = input(false); bla() { const shared = this.shared(); @@ -837,7 +855,7 @@ import { Directive, Component, input } from '@angular/core'; @Directive() class SomeDir { - bla = input.required(); + readonly bla = input.required(); } @Component({ @@ -931,8 +949,8 @@ export class MyComp { @Input() first = true; @Input() second = false; @Input() third = true; - fourth = input(true); - fifth = input(true); + readonly fourth = input(true); + readonly fifth = input(true); } @@@@@@ template_object_shorthand.ts @@@@@@ @@ -950,7 +968,7 @@ import { Component, input } from '@angular/core'; }, }) export class TemplateObjectShorthand { - myInput = input(true); + readonly myInput = input(true); } @@@@@@ template_writes.ts @@@@@@ @@ -972,7 +990,7 @@ class TwoWayBinding { @Input() inputA = ''; @Input() inputB = true; @Input() inputC = false; - inputD = input(false); + readonly inputD = input(false); } @@@@@@ transform_functions.ts @@@@@@ @@ -987,12 +1005,12 @@ function x(v: string | undefined): string | undefined { export class TransformFunctions { // We can check this, and expect `as any` due to transform incompatibility. - withExplicitTypeWorks = input.required<{ + readonly withExplicitTypeWorks = input.required<{ ok: true; }, string | undefined>({ transform: ((v: string | undefined) => '') as any }); // This will be a synthetic type because we add `undefined` to `boolean`. - synthetic1 = input.required({ transform: x }); + readonly synthetic1 = input.required({ transform: x }); // Synthetic as we infer a full type from the initial value. Cannot be checked. @Input({required: true, transform: (v: string | undefined) => ''}) synthetic2 = { infer: COMPLEX_VAR, @@ -1009,7 +1027,7 @@ import { Directive, input } from '@angular/core'; @Directive() class TransformIncompatibleTypes { // @ts-ignore Simulate `--strictPropertyInitialization=false`. - disabled = input(undefined, { transform: ((v: unknown) => (v === null ? undefined : !!v)) as any }); + readonly disabled = input(undefined, { transform: ((v: unknown) => (v === null ? undefined : !!v)) as any }); } @@@@@@ with_getters.ts @@@@@@ @@ -1036,7 +1054,7 @@ export class WithSettersAndGetters { } private _accessor: string = ''; - simpleInput = input.required(); + readonly simpleInput = input.required(); } @@@@@@ with_getters_reference.ts @@@@@@ @@ -1063,7 +1081,7 @@ class WithJsdoc { /** * Works */ - simpleInput = input.required(); + readonly simpleInput = input.required(); - withCommentInside = input(); + readonly withCommentInside = input(); } diff --git a/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt b/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt index 12d803c9634..905335c0a9a 100644 --- a/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt +++ b/packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt @@ -9,11 +9,11 @@ import {By} from '@angular/platform-browser'; function it(msg: string, fn: () => void) {} class SubDir { - name = input('John'); + readonly name = input('John'); } class MyComp { - hello = input(''); + readonly hello = input(''); } it('should work', () => { @@ -36,16 +36,16 @@ class BaseNonAngular { @Directive() class Sub implements BaseNonAngular { // should not be migrated because of the interface. - disabled = input(''); + readonly disabled = input(''); } class BaseWithAngular { - disabled = input(''); + readonly disabled = input(''); } @Directive() class Sub2 extends BaseWithAngular { - disabled = input(''); + readonly disabled = input(''); } interface BaseNonAngularInterface { @@ -55,7 +55,7 @@ interface BaseNonAngularInterface { @Directive() class Sub3 implements BaseNonAngularInterface { // should not be migrated because of the interface. - disabled = input(''); + readonly disabled = input(''); } @@@@@@ both_input_imports.ts @@@@@@ @@ -64,10 +64,10 @@ class Sub3 implements BaseNonAngularInterface { import { input } from '@angular/core'; class BothInputImported { - decoratorInput = input(true); + readonly decoratorInput = input(true); signalInput = input(); - thisCanBeMigrated = input(true); + readonly thisCanBeMigrated = input(true); __makeDecoratorInputNonMigratable() { this.decoratorInput = false; @@ -87,7 +87,7 @@ function it(msg: string, fn: () => void) {} function bootstrapTemplate(tmpl: string, inputs: unknown) {} class MyComp { - hello = input(''); + readonly hello = input(''); } it('should work', () => { @@ -109,7 +109,7 @@ import { Component, input } from '@angular/core'; `, }) class Group { - label = input.required(); + readonly label = input.required(); } @Component({ @@ -132,7 +132,7 @@ import { Directive, input } from '@angular/core'; @Directive() class Base { - bla = input(true); + readonly bla = input(true); } class Derived extends Base { @@ -142,7 +142,7 @@ class Derived extends Base { // overridden in separate file @Directive() export class Base2 { - bla = input(true); + readonly bla = input(true); } @@@@@@ derived_class_meta_input_alias.ts @@@@@@ @@ -153,7 +153,7 @@ import { Directive, input } from '@angular/core'; @Directive() class Base { // should not be migrated. - bla = input(true); + readonly bla = input(true); } @Directive({ @@ -182,7 +182,7 @@ let nextUniqueId = 0; @Directive() export class MatHint { align: string = ''; - id = input(`mat-hint-${nextUniqueId++}`); + readonly id = input(`mat-hint-${nextUniqueId++}`); } @Component({ @@ -226,7 +226,7 @@ import { input } from '@angular/core'; class ExistingSignalImport { signalInput = input(); - thisCanBeMigrated = input(true); + readonly thisCanBeMigrated = input(true); } @@@@@@ getters.ts @@@@@@ @@ -266,7 +266,7 @@ import { Component, input } from '@angular/core'; }, }) class HostBindingTestCmp { - id = input('works'); + readonly id = input('works'); // for testing nested expressions. nested = this; @@ -284,7 +284,7 @@ const SHARED = { host: SHARED, }) class HostBindingsShared { - id = input(false); + readonly id = input(false); } @Component({ @@ -292,8 +292,8 @@ class HostBindingsShared { host: SHARED, }) class HostBindingsShared2 { - id = input(false); - id2 = input(false); + readonly id = input(false); + readonly id2 = input(false); } @@@@@@ identifier_collisions.ts @@@@@@ @@ -313,8 +313,8 @@ const complex = 'some global variable'; @Component({template: ''}) class MyComp { - name = input(null); - complex = input(null); + readonly name = input(null); + readonly complex = input(null); valid() { const name = this.name(); @@ -371,11 +371,11 @@ interface Audi extends Car { templateUrl: './template.html', }) export class AppComponent { - input = input(null); - bla = input.required({ transform: disabledTransform }); - narrowableMultipleTimes = input(null); - withUndefinedInput = input(); - incompatible = input(null); + readonly input = input(null); + readonly bla = input.required({ transform: disabledTransform }); + readonly narrowableMultipleTimes = input(null); + readonly withUndefinedInput = input(); + readonly incompatible = input(null); private _bla: any; @Input() @@ -496,7 +496,7 @@ import { Component, input } from '@angular/core'; @Component({template: ''}) class IndexAccessInput { - items = input([]); + readonly items = input([]); bla() { const {items} = this; @@ -558,7 +558,7 @@ import { Component, input } from '@angular/core'; `, }) export class InlineTmpl { - justify = input<'start' | 'end'>('end'); + readonly justify = input<'start' | 'end'>('end'); } @@@@@@ jit_true_component_external_tmpl.html @@@@@@ @@ -582,7 +582,7 @@ import { Component, input } from '@angular/core'; template: '{{test()}}', }) class JitTrueComponent { - test = input(true); + readonly test = input(true); } @Component({ @@ -590,7 +590,7 @@ class JitTrueComponent { templateUrl: './jit_true_component_external_tmpl.html', }) class JitTrueComponentExternalTmpl { - test = input(true); + readonly test = input(true); } @@@@@@ loop_labels.ts @@@@@@ @@ -599,7 +599,7 @@ class JitTrueComponentExternalTmpl { import { input } from '@angular/core'; class MyTestCmp { - someInput = input.required(); + readonly someInput = input.required(); tmpValue = false; @@ -638,6 +638,24 @@ import {Component, Input} from '@angular/core'; export class ManualInstantiation { @Input() bla: string = ''; } +@@@@@@ modifier_tests.ts @@@@@@ + +// tslint:disable + +import { Component, input } from '@angular/core'; + +function CustomDecorator() { + return (a: any, b: any) => {}; +} + +@Component({template: ''}) +class ModifierScenarios { + readonly alreadyReadonly = input(true); + protected readonly ImProtected = input(true); + protected readonly ImProtectedAndReadonly = input(true); + @CustomDecorator() +protected readonly usingCustomDecorator = input(true); +} @@@@@@ mutate.ts @@@@@@ // tslint:disable @@ -645,7 +663,7 @@ export class ManualInstantiation { import { input } from '@angular/core'; export class TestCmp { - shared = input<{ + readonly shared = input<{ x: string; }>({ x: '' }); @@ -678,7 +696,7 @@ interface Config { `, }) export class NestedTemplatePropAccess { - config = input({}); + readonly config = input({}); } @@@@@@ object_expansion.ts @@@@@@ @@ -688,7 +706,7 @@ import { Component, input } from '@angular/core'; @Component({}) export class ObjectExpansion { - bla = input(''); + readonly bla = input(''); expansion() { const {bla} = this; @@ -720,7 +738,7 @@ import { Directive, input } from '@angular/core'; @Directive() class OptionalInput { - bla = input(); + readonly bla = input(); } @@@@@@ problematic_type_reference.ts @@@@@@ @@ -734,7 +752,7 @@ import { Component, Directive, QueryList, input } from '@angular/core'; `, }) class Group { - label = input.required(); + readonly label = input.required(); } @Directive() @@ -767,12 +785,12 @@ import {COMPLEX_VAR} from './required-no-explicit-type-extra'; export const CONST = {field: true}; export class RequiredNoExplicitType { - someInputNumber = input.required(); - someInput = input.required(); - withConstInitialVal = input.required(); + readonly someInputNumber = input.required(); + readonly someInput = input.required(); + readonly withConstInitialVal = input.required(); // typing this explicitly now would require same imports as from the `-extra` file. - complexVal = input.required(); + readonly complexVal = input.required(); } @@@@@@ required.ts @@@@@@ @@ -781,7 +799,7 @@ export class RequiredNoExplicitType { import { input } from '@angular/core'; class Required { - simpleInput = input.required(); + readonly simpleInput = input.required(); } @@@@@@ safe_property_reads.ts @@@@@@ @@ -803,7 +821,7 @@ import { Component, input } from '@angular/core'; `, }) class WithSafePropertyReads { - myInput = input(0); + readonly myInput = input(0); bla: this | undefined = this; } @@ -814,7 +832,7 @@ class WithSafePropertyReads { import { input } from '@angular/core'; export class TestCmp { - shared = input(false); + readonly shared = input(false); bla() { const shared = this.shared(); @@ -837,7 +855,7 @@ import { Directive, Component, input } from '@angular/core'; @Directive() class SomeDir { - bla = input.required(); + readonly bla = input.required(); } @Component({ @@ -928,11 +946,11 @@ import { Component, input } from '@angular/core'; `, }) export class MyComp { - first = input(true); - second = input(false); - third = input(true); - fourth = input(true); - fifth = input(true); + readonly first = input(true); + readonly second = input(false); + readonly third = input(true); + readonly fourth = input(true); + readonly fifth = input(true); } @@@@@@ template_object_shorthand.ts @@@@@@ @@ -950,7 +968,7 @@ import { Component, input } from '@angular/core'; }, }) export class TemplateObjectShorthand { - myInput = input(true); + readonly myInput = input(true); } @@@@@@ template_writes.ts @@@@@@ @@ -969,10 +987,10 @@ import { Component, input } from '@angular/core'; }, }) class TwoWayBinding { - inputA = input(''); - inputB = input(true); - inputC = input(false); - inputD = input(false); + readonly inputA = input(''); + readonly inputB = input(true); + readonly inputC = input(false); + readonly inputD = input(false); } @@@@@@ transform_functions.ts @@@@@@ @@ -987,12 +1005,12 @@ function x(v: string | undefined): string | undefined { export class TransformFunctions { // We can check this, and expect `as any` due to transform incompatibility. - withExplicitTypeWorks = input.required<{ + readonly withExplicitTypeWorks = input.required<{ ok: true; }, string | undefined>({ transform: ((v: string | undefined) => '') as any }); // This will be a synthetic type because we add `undefined` to `boolean`. - synthetic1 = input.required({ transform: x }); + readonly synthetic1 = input.required({ transform: x }); // Synthetic as we infer a full type from the initial value. Cannot be checked. @Input({required: true, transform: (v: string | undefined) => ''}) synthetic2 = { infer: COMPLEX_VAR, @@ -1009,7 +1027,7 @@ import { Directive, input } from '@angular/core'; @Directive() class TransformIncompatibleTypes { // @ts-ignore Simulate `--strictPropertyInitialization=false`. - disabled = input(undefined, { transform: ((v: unknown) => (v === null ? undefined : !!v)) as any }); + readonly disabled = input(undefined, { transform: ((v: unknown) => (v === null ? undefined : !!v)) as any }); } @@@@@@ with_getters.ts @@@@@@ @@ -1036,7 +1054,7 @@ export class WithSettersAndGetters { } private _accessor: string = ''; - simpleInput = input.required(); + readonly simpleInput = input.required(); } @@@@@@ with_getters_reference.ts @@@@@@ @@ -1063,7 +1081,7 @@ class WithJsdoc { /** * Works */ - simpleInput = input.required(); + readonly simpleInput = input.required(); - withCommentInside = input(); + readonly withCommentInside = input(); }