@dsivd/prestations-ng 19.0.7 → 19.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Classification of a signal member, shared by the TypeScript-API resolution of
3
+ * `signal-names.mjs` and the ESLint-AST checks of the rules, which read the same names out
4
+ * of two different trees.
5
+ *
6
+ * The value of a signal is never to be mutated, whatever its kind — a mutation notifies
7
+ * none of its dependents. `VIEW_QUERY` is the single exception: it hands out a DOM element
8
+ * or a component instance, and mutating that is the reason for asking for it.
9
+ *
10
+ * The kinds beyond `WRITABLE` differ only in the advice that applies, since none of them
11
+ * has a `.set()`.
12
+ */
13
+
14
+ export const WRITABLE = "writable";
15
+ export const COMPUTED = "computed";
16
+ export const INPUT = "input";
17
+ export const FROM_OBSERVABLE = "fromObservable";
18
+ export const VIEW_QUERY = "viewQuery";
19
+ // A read-only signal whose factory its declaration does not name: it may be a view query.
20
+ export const AMBIGUOUS = "ambiguous";
21
+
22
+ // `output()` is absent: an output is never called, it is emitted.
23
+ const KIND_BY_FACTORY = new Map([
24
+ ["signal", WRITABLE],
25
+ ["model", WRITABLE],
26
+ ["linkedSignal", WRITABLE],
27
+ ["computed", COMPUTED],
28
+ ["input", INPUT],
29
+ ["toSignal", FROM_OBSERVABLE],
30
+ ["viewChild", VIEW_QUERY],
31
+ ["viewChildren", VIEW_QUERY],
32
+ ["contentChild", VIEW_QUERY],
33
+ ["contentChildren", VIEW_QUERY],
34
+ ]);
35
+
36
+ /**
37
+ * Declaration files qualify the types with the namespace they were imported under —
38
+ * `readonly icon: _angular_core.ModelSignal<IconProp>` — hence the comparison on the last
39
+ * segment only.
40
+ *
41
+ * `Signal<T>` is where a published declaration loses the information: a `computed()`, a
42
+ * `toSignal()` and a view query all end up spelled that way. It has to stay `AMBIGUOUS`,
43
+ * because this library alone publishes 54 view queries — `AbstractPageComponent` and
44
+ * `FoehnInputComponent` among them — and reporting them would break every project that
45
+ * extends one and touches the element it queried.
46
+ */
47
+ const KIND_BY_TYPE = new Map([
48
+ ["WritableSignal", WRITABLE],
49
+ ["ModelSignal", WRITABLE],
50
+ ["InputSignal", INPUT],
51
+ ["InputSignalWithTransform", INPUT],
52
+ ["Signal", AMBIGUOUS],
53
+ ]);
54
+
55
+ export const factoryKind = (name) => KIND_BY_FACTORY.get(name);
56
+
57
+ export const typeKind = (name) => KIND_BY_TYPE.get(name);
@@ -2,8 +2,10 @@ import { readFileSync, statSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import ts from "typescript";
4
4
 
5
+ import { factoryKind, typeKind } from "./signal-kinds.mjs";
6
+
5
7
  /**
6
- * Collects the signal member names of a component, walking up its inheritance chain.
8
+ * Collects the signal members of a component, walking up its inheritance chain.
7
9
  *
8
10
  * Base classes are resolved automatically: the `extends` clause is matched against the
9
11
  * imports of the file, the module is resolved the way TypeScript would (relative paths,
@@ -11,31 +13,6 @@ import ts from "typescript";
11
13
  * to be declared by hand.
12
14
  */
13
15
 
14
- // `output()` is absent: an output is never called, it is emitted.
15
- const SIGNAL_FACTORIES = new Set([
16
- "input",
17
- "model",
18
- "computed",
19
- "signal",
20
- "linkedSignal",
21
- "toSignal",
22
- "viewChild",
23
- "viewChildren",
24
- "contentChild",
25
- "contentChildren",
26
- ]);
27
-
28
- // Types of a signal member. Declaration files qualify them with the namespace they were
29
- // imported under: `readonly icon: _angular_core.ModelSignal<IconProp>`, hence the
30
- // comparison on the last segment only.
31
- const SIGNAL_TYPES = new Set([
32
- "Signal",
33
- "WritableSignal",
34
- "InputSignal",
35
- "InputSignalWithTransform",
36
- "ModelSignal",
37
- ]);
38
-
39
16
  const parsedFiles = new Map();
40
17
  const compilerOptionsByDirectory = new Map();
41
18
 
@@ -96,31 +73,33 @@ const resolveModule = (specifier, containingFile) =>
96
73
  const lastSegmentOf = (entityName) =>
97
74
  ts.isQualifiedName(entityName) ? entityName.right.text : entityName.text;
98
75
 
99
- const isSignalType = (typeNode) =>
100
- !!typeNode &&
101
- ts.isTypeReferenceNode(typeNode) &&
102
- SIGNAL_TYPES.has(lastSegmentOf(typeNode.typeName));
76
+ const kindOfType = (typeNode) =>
77
+ typeNode && ts.isTypeReferenceNode(typeNode)
78
+ ? typeKind(lastSegmentOf(typeNode.typeName))
79
+ : undefined;
103
80
 
104
- const isSignalFactoryCall = (expression) => {
81
+ const kindOfInitializer = (expression) => {
105
82
  if (!expression || !ts.isCallExpression(expression)) {
106
- return false;
83
+ return undefined;
107
84
  }
108
85
  // `input.required<T>()` and `viewChild.required(…)` unwrap to their factory.
109
86
  let callee = expression.expression;
110
87
  while (ts.isPropertyAccessExpression(callee)) {
111
88
  callee = callee.expression;
112
89
  }
113
- return ts.isIdentifier(callee) && SIGNAL_FACTORIES.has(callee.text);
90
+ return ts.isIdentifier(callee) ? factoryKind(callee.text) : undefined;
114
91
  };
115
92
 
116
- const isSignalMember = (member) => {
93
+ // Kind of signal a class member holds, `undefined` when it holds none. The initializer
94
+ // comes first: it names the factory, where the type may only say `Signal<T>`.
95
+ const kindOfMember = (member) => {
117
96
  if (ts.isGetAccessor(member)) {
118
- return isSignalType(member.type);
97
+ return kindOfType(member.type);
119
98
  }
120
- return (
121
- ts.isPropertyDeclaration(member) &&
122
- (isSignalType(member.type) || isSignalFactoryCall(member.initializer))
123
- );
99
+ if (!ts.isPropertyDeclaration(member)) {
100
+ return undefined;
101
+ }
102
+ return kindOfInitializer(member.initializer) ?? kindOfType(member.type);
124
103
  };
125
104
 
126
105
  const findClass = (sourceFile, className) => {
@@ -174,7 +153,7 @@ const reExportedModulesOf = (sourceFile) =>
174
153
  .filter((statement) => ts.isExportDeclaration(statement) && statement.moduleSpecifier)
175
154
  .map((statement) => statement.moduleSpecifier.text);
176
155
 
177
- const collect = (path, className, names, visited) => {
156
+ const collect = (path, className, kinds, visited) => {
178
157
  const key = `${path}::${className ?? "*"}`;
179
158
  if (visited.has(key)) {
180
159
  return;
@@ -195,7 +174,7 @@ const collect = (path, className, names, visited) => {
195
174
  for (const specifier of reExportedModulesOf(sourceFile)) {
196
175
  const target = resolveModule(specifier, path);
197
176
  if (target) {
198
- collect(target, className, names, visited);
177
+ collect(target, className, kinds, visited);
199
178
  }
200
179
  }
201
180
  return;
@@ -203,8 +182,13 @@ const collect = (path, className, names, visited) => {
203
182
 
204
183
  for (const classDeclaration of classDeclarations) {
205
184
  for (const member of classDeclaration.members) {
206
- if (member.name && ts.isIdentifier(member.name) && isSignalMember(member)) {
207
- names.add(member.name.text);
185
+ if (!member.name || !ts.isIdentifier(member.name)) {
186
+ continue;
187
+ }
188
+ const kind = kindOfMember(member);
189
+ // A subclass overriding a member wins: it is collected first.
190
+ if (kind && !kinds.has(member.name.text)) {
191
+ kinds.set(member.name.text, kind);
208
192
  }
209
193
  }
210
194
 
@@ -214,19 +198,29 @@ const collect = (path, className, names, visited) => {
214
198
  }
215
199
  // Declaration bundles are flattened: the base class often sits in the same file.
216
200
  if (findClass(sourceFile, baseName)) {
217
- collect(path, baseName, names, visited);
201
+ collect(path, baseName, kinds, visited);
218
202
  continue;
219
203
  }
220
204
  const imported = importOf(sourceFile, baseName);
221
205
  const target = imported && resolveModule(imported.specifier, path);
222
206
  if (target) {
223
- collect(target, imported.exportedName, names, visited);
207
+ collect(target, imported.exportedName, kinds, visited);
224
208
  }
225
209
  }
226
210
  };
227
211
 
228
- export const collectSignalNames = (componentPath) => {
229
- const names = new Set();
230
- collect(componentPath, undefined, names, new Set());
231
- return names;
212
+ /**
213
+ * Signal members of a component, mapped to their kind. `undefined` when the file
214
+ * cannot be read: unknown signals must be distinguished from a component that has none.
215
+ */
216
+ export const collectSignalKinds = (componentPath) => {
217
+ if (!parseFile(componentPath)) {
218
+ return undefined;
219
+ }
220
+ const kinds = new Map();
221
+ collect(componentPath, undefined, kinds, new Set());
222
+ return kinds;
232
223
  };
224
+
225
+ export const collectSignalNames = (componentPath) =>
226
+ new Set(collectSignalKinds(componentPath)?.keys() ?? []);