@taiga-ui/eslint-plugin-experience-next 0.469.0 → 0.470.0
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.
- package/README.md +33 -12
- package/index.esm.js +30 -5
- package/package.json +1 -1
- package/rules/utils/angular-signals.d.ts +2 -0
package/README.md
CHANGED
|
@@ -54,9 +54,9 @@ export default [
|
|
|
54
54
|
| no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | ✅ | | |
|
|
55
55
|
| no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
|
|
56
56
|
| no-side-effects-in-computed | Disallow observable side effects inside Angular `computed()` callbacks | ✅ | | |
|
|
57
|
-
| no-signal-reads-after-await-in-reactive-context | Disallow signal reads after `await` inside reactive callbacks
|
|
57
|
+
| no-signal-reads-after-await-in-reactive-context | Disallow bare signal reads after `await` inside reactive callbacks | ✅ | | |
|
|
58
58
|
| no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
|
|
59
|
-
| no-untracked-outside-reactive-context | Disallow `untracked()` outside
|
|
59
|
+
| no-untracked-outside-reactive-context | Disallow `untracked()` outside reactive callbacks, except explicit post-`await` snapshots | ✅ | 🔧 | |
|
|
60
60
|
| no-useless-untracked | Disallow provably useless `untracked()` wrappers in reactive callbacks | ✅ | 🔧 | |
|
|
61
61
|
| object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
|
|
62
62
|
| prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
|
|
@@ -602,8 +602,9 @@ const derived = computed(() => source() + 1);
|
|
|
602
602
|
|
|
603
603
|
<sup>`✅ Recommended`</sup>
|
|
604
604
|
|
|
605
|
-
Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any signal
|
|
606
|
-
after `await` will not become a dependency.
|
|
605
|
+
Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any bare signal
|
|
606
|
+
read after `await` will not become a dependency. Snapshot before `await` when you need the earlier value, or make an
|
|
607
|
+
intentional post-`await` current-value read explicit with `untracked(...)`.
|
|
607
608
|
|
|
608
609
|
```ts
|
|
609
610
|
// ❌ error
|
|
@@ -620,6 +621,14 @@ effect(async () => {
|
|
|
620
621
|
});
|
|
621
622
|
```
|
|
622
623
|
|
|
624
|
+
```ts
|
|
625
|
+
// ✅ ok — explicit current-value read after await
|
|
626
|
+
effect(async () => {
|
|
627
|
+
await this.fetchUser();
|
|
628
|
+
console.log(untracked(this.theme));
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
623
632
|
---
|
|
624
633
|
|
|
625
634
|
## no-untracked-outside-reactive-context
|
|
@@ -627,14 +636,15 @@ effect(async () => {
|
|
|
627
636
|
<sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
|
|
628
637
|
|
|
629
638
|
`untracked()` usually only affects signal reads that happen inside the synchronous body of a reactive callback. In
|
|
630
|
-
ordinary non-reactive code
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
`registerOnChange` including patched accessors such as
|
|
634
|
-
inside deferred scheduler / event-handler callbacks,
|
|
635
|
-
`InjectionToken({factory})` / `useFactory` when they guard creation of a
|
|
636
|
-
accidental ambient reactive context. For the narrow case
|
|
637
|
-
around a reactive call in ordinary code, autofix removes only
|
|
639
|
+
ordinary non-reactive code or nested callbacks it usually does not prevent dependency tracking and only adds noise. This
|
|
640
|
+
rule reports those cases, but intentionally allows a few explicit escape hatches: post-`await` reads inside a reactive
|
|
641
|
+
callback when `untracked()` is used to document an intentional current-value snapshot, imperative Angular hooks such as
|
|
642
|
+
`@Pipe().transform`, `ControlValueAccessor.writeValue`, `registerOnChange` including patched accessors such as
|
|
643
|
+
`accessor.writeValue = (...) => {}`, callback-form wrappers used inside deferred scheduler / event-handler callbacks,
|
|
644
|
+
and narrow lazy DI factory wrappers like `InjectionToken({factory})` / `useFactory` when they guard creation of a
|
|
645
|
+
reactive owner such as `effect()` against an accidental ambient reactive context. For the narrow case
|
|
646
|
+
`untracked(() => effect(...))` and similar outer wrappers around a reactive call in ordinary code, autofix removes only
|
|
647
|
+
the useless outer `untracked()` wrapper.
|
|
638
648
|
|
|
639
649
|
```ts
|
|
640
650
|
// ❌ error
|
|
@@ -656,6 +666,17 @@ effect(() => {
|
|
|
656
666
|
const snapshot = computed(() => untracked(this.user));
|
|
657
667
|
```
|
|
658
668
|
|
|
669
|
+
```ts
|
|
670
|
+
// ✅ ok — after await, untracked can mark an intentional current snapshot
|
|
671
|
+
effect(async () => {
|
|
672
|
+
await this.refresh();
|
|
673
|
+
|
|
674
|
+
if (untracked(this.user) !== previousUser) {
|
|
675
|
+
console.log('changed');
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
659
680
|
```ts
|
|
660
681
|
// ❌ error
|
|
661
682
|
untracked(() => {
|
package/index.esm.js
CHANGED
|
@@ -2397,6 +2397,16 @@ function isNodeInsideSynchronousReactiveScope(node, callback) {
|
|
|
2397
2397
|
});
|
|
2398
2398
|
return found;
|
|
2399
2399
|
}
|
|
2400
|
+
function isNodeAfterAsyncBoundaryInReactiveScope(node, callback) {
|
|
2401
|
+
let found = false;
|
|
2402
|
+
walkAfterAsyncBoundaryAst(callback, (inner) => {
|
|
2403
|
+
if (inner !== node) {
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
found = true;
|
|
2407
|
+
});
|
|
2408
|
+
return found;
|
|
2409
|
+
}
|
|
2400
2410
|
function findEnclosingReactiveScope(node, program) {
|
|
2401
2411
|
for (let current = node.parent; current; current = current.parent) {
|
|
2402
2412
|
if (current.type !== AST_NODE_TYPES.CallExpression) {
|
|
@@ -2410,6 +2420,19 @@ function findEnclosingReactiveScope(node, program) {
|
|
|
2410
2420
|
}
|
|
2411
2421
|
return null;
|
|
2412
2422
|
}
|
|
2423
|
+
function findEnclosingReactiveScopeAfterAsyncBoundary(node, program) {
|
|
2424
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2425
|
+
if (current.type !== AST_NODE_TYPES.CallExpression) {
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
for (const scope of getReactiveScopes(current, program)) {
|
|
2429
|
+
if (isNodeAfterAsyncBoundaryInReactiveScope(node, scope.callback)) {
|
|
2430
|
+
return scope;
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2413
2436
|
/**
|
|
2414
2437
|
* Returns true when the TypeScript type at `node` is an Angular signal type.
|
|
2415
2438
|
* Uses duck-typing: callable type whose name contains "Signal", or whose
|
|
@@ -3256,6 +3279,7 @@ const rule$9 = createUntrackedRule({
|
|
|
3256
3279
|
const reported = new Set();
|
|
3257
3280
|
walkAfterAsyncBoundaryAst(scope.callback, (inner) => {
|
|
3258
3281
|
if (inner.type !== AST_NODE_TYPES.CallExpression ||
|
|
3282
|
+
isAngularUntrackedCall(inner, program) ||
|
|
3259
3283
|
!isSignalReadCall(inner, checker, esTreeNodeToTSNodeMap)) {
|
|
3260
3284
|
return;
|
|
3261
3285
|
}
|
|
@@ -3279,11 +3303,11 @@ const rule$9 = createUntrackedRule({
|
|
|
3279
3303
|
},
|
|
3280
3304
|
meta: {
|
|
3281
3305
|
docs: {
|
|
3282
|
-
description: 'Disallow signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
|
|
3306
|
+
description: 'Disallow bare signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
|
|
3283
3307
|
url: ANGULAR_SIGNALS_ASYNC_GUIDE_URL,
|
|
3284
3308
|
},
|
|
3285
3309
|
messages: {
|
|
3286
|
-
readAfterAwait: '`{{ name }}` is read after `await` inside `{{ kind }}`. Angular only tracks synchronous signal reads, so this dependency will not be tracked. Read it before `await` and store the snapshot. See Angular guide: https://angular.dev/guide/signals#reactive-context-and-async-operations',
|
|
3310
|
+
readAfterAwait: '`{{ name }}` is read after `await` inside `{{ kind }}`. Angular only tracks synchronous signal reads, so this dependency will not be tracked. Read it before `await` and store the snapshot, or wrap the post-`await` read in `untracked(...)` when you intentionally need the current value at that point. See Angular guide: https://angular.dev/guide/signals#reactive-context-and-async-operations',
|
|
3287
3311
|
},
|
|
3288
3312
|
schema: [],
|
|
3289
3313
|
type: 'problem',
|
|
@@ -3802,7 +3826,8 @@ const rule$7 = createUntrackedRule({
|
|
|
3802
3826
|
if (!isAngularUntrackedCall(node, program)) {
|
|
3803
3827
|
return;
|
|
3804
3828
|
}
|
|
3805
|
-
if (findEnclosingReactiveScope(node, program)
|
|
3829
|
+
if (findEnclosingReactiveScope(node, program) ||
|
|
3830
|
+
findEnclosingReactiveScopeAfterAsyncBoundary(node, program)) {
|
|
3806
3831
|
return;
|
|
3807
3832
|
}
|
|
3808
3833
|
if (isAllowedImperativeAngularContext(node)) {
|
|
@@ -3840,12 +3865,12 @@ const rule$7 = createUntrackedRule({
|
|
|
3840
3865
|
},
|
|
3841
3866
|
meta: {
|
|
3842
3867
|
docs: {
|
|
3843
|
-
description: 'Disallow `untracked()` outside
|
|
3868
|
+
description: 'Disallow `untracked()` outside reactive callbacks, except for synchronous reactive reads, explicit post-`await` snapshot reads, and supported imperative/deferred/lazy-factory Angular escape hatches',
|
|
3844
3869
|
url: ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL,
|
|
3845
3870
|
},
|
|
3846
3871
|
fixable: 'code',
|
|
3847
3872
|
messages: {
|
|
3848
|
-
outsideReactiveContext: '`untracked()` is used outside
|
|
3873
|
+
outsideReactiveContext: '`untracked()` is used outside a reactive callback and outside the supported post-`await` / imperative / deferred / lazy-factory exceptions, so it does not prevent dependency tracking and only adds noise. Remove it. See Angular guide: https://angular.dev/guide/signals#reading-without-tracking-dependencies',
|
|
3849
3874
|
},
|
|
3850
3875
|
schema: [],
|
|
3851
3876
|
type: 'problem',
|
package/package.json
CHANGED
|
@@ -20,7 +20,9 @@ export declare function isAngularEffectCall(node: TSESTree.CallExpression, progr
|
|
|
20
20
|
export declare function isAngularUntrackedCall(node: TSESTree.CallExpression, program: TSESTree.Program): boolean;
|
|
21
21
|
export declare function getReactiveScopes(node: TSESTree.CallExpression, program: TSESTree.Program): ReactiveScope[];
|
|
22
22
|
export declare function isNodeInsideSynchronousReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|
|
23
|
+
export declare function isNodeAfterAsyncBoundaryInReactiveScope(node: TSESTree.Node, callback: ReactiveCallback): boolean;
|
|
23
24
|
export declare function findEnclosingReactiveScope(node: TSESTree.Node, program: TSESTree.Program): ReactiveScope | null;
|
|
25
|
+
export declare function findEnclosingReactiveScopeAfterAsyncBoundary(node: TSESTree.Node, program: TSESTree.Program): ReactiveScope | null;
|
|
24
26
|
/**
|
|
25
27
|
* Returns true when the TypeScript type at `node` is an Angular signal type.
|
|
26
28
|
* Uses duck-typing: callable type whose name contains "Signal", or whose
|