Skip to content

Commit bbc970b

Browse files
devversionAndrewKushnir
authored andcommitted
refactor(migrations): always add readonly to migrated signal inputs (#57368)
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
1 parent 87d00d2 commit bbc970b

File tree

6 files changed

+180
-111
lines changed

6 files changed

+180
-111
lines changed

packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export function convertToSignalInput(
3232
resolvedType,
3333
preferShorthandIfPossible,
3434
isUndefinedInitialValue,
35+
originalInputDecorator,
3536
}: ConvertInputPreparation,
3637
checker: ts.TypeChecker,
3738
importManager: ImportManager,
@@ -117,12 +118,16 @@ export function convertToSignalInput(
117118
inputArgs,
118119
);
119120

120-
// TODO:
121-
// - modifiers (but private does not work)
122-
// - preserve custom decorators etc.
121+
let modifiersWithoutInputDecorator =
122+
node.modifiers?.filter((m) => m !== originalInputDecorator.node) ?? [];
123+
124+
// Add `readonly` to all new signal input declarations.
125+
if (!modifiersWithoutInputDecorator?.some((s) => s.kind === ts.SyntaxKind.ReadonlyKeyword)) {
126+
modifiersWithoutInputDecorator.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword));
127+
}
123128

124129
const result = ts.factory.createPropertyDeclaration(
125-
undefined,
130+
modifiersWithoutInputDecorator,
126131
node.name,
127132
undefined,
128133
undefined,

packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
InputMemberIncompatibility,
1414
} from '../input_detection/incompatibility';
1515
import {InputNode} from '../input_detection/input_node';
16+
import {Decorator} from '../../../../../../compiler-cli/src/ngtsc/reflection';
17+
import assert from 'assert';
1618

1719
/**
1820
* Interface describing analysis performed when the input
@@ -23,6 +25,7 @@ export interface ConvertInputPreparation {
2325
preferShorthandIfPossible: {originalType: ts.TypeNode} | null;
2426
isUndefinedInitialValue: boolean;
2527
resolvedMetadata: ExtractedInput;
28+
originalInputDecorator: Decorator;
2629
}
2730

2831
/**
@@ -46,6 +49,12 @@ export function prepareAndCheckForConversion(
4649
reason: InputIncompatibilityReason.Accessor,
4750
};
4851
}
52+
53+
assert(
54+
metadata.inputDecorator !== null,
55+
'Expected an input decorator for inputs that are being migrated.',
56+
);
57+
4958
const initialValue = node.initializer;
5059

5160
// If an input can be required, due to the non-null assertion on the property,
@@ -118,5 +127,6 @@ export function prepareAndCheckForConversion(
118127
resolvedType: typeToAdd,
119128
preferShorthandIfPossible,
120129
isUndefinedInitialValue,
130+
originalInputDecorator: metadata.inputDecorator,
121131
};
122132
}

packages/core/schematics/migrations/signal-migration/src/input_detection/input_decorator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '../../../../../../compiler-cli/src/ngtsc/partial_evaluator';
2525
import {
2626
ClassDeclaration,
27-
DecoratorIdentifier,
27+
Decorator,
2828
ReflectionHost,
2929
} from '../../../../../../compiler-cli/src/ngtsc/reflection';
3030
import {CompilationMode} from '../../../../../../compiler-cli/src/ngtsc/transform';
@@ -34,6 +34,7 @@ import {InputNode, isInputContainerNode} from '../input_detection/input_node';
3434
/** Metadata extracted of an input declaration (in `.ts` or `.d.ts` files). */
3535
export interface ExtractedInput extends InputMapping {
3636
inSourceFile: boolean;
37+
inputDecorator: Decorator | null;
3738
}
3839

3940
/** Attempts to extract metadata of a potential TypeScript `@Input()` declaration. */
@@ -86,6 +87,7 @@ function extractDtsInput(node: ts.Node, metadataReader: DtsMetadataReader): Extr
8687
? null
8788
: {
8889
...inputMapping,
90+
inputDecorator: null,
8991
inSourceFile: false,
9092
};
9193
}
@@ -150,6 +152,7 @@ function extractSourceCodeInput(
150152
isSignal: false,
151153
inSourceFile: true,
152154
transform: transformResult,
155+
inputDecorator,
153156
};
154157
}
155158

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// tslint:disable
2+
3+
import {Component, Input} from '@angular/core';
4+
5+
function CustomDecorator() {
6+
return (a: any, b: any) => {};
7+
}
8+
9+
@Component({template: ''})
10+
class ModifierScenarios {
11+
@Input() readonly alreadyReadonly = true;
12+
@Input() protected ImProtected = true;
13+
@Input() protected readonly ImProtectedAndReadonly = true;
14+
@Input() @CustomDecorator() protected readonly usingCustomDecorator = true;
15+
}

0 commit comments

Comments
 (0)