@taiga-ui/eslint-plugin-experience-next 0.469.0 → 0.471.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 CHANGED
@@ -43,6 +43,7 @@ export default [
43
43
  | class-property-naming | Enforce custom naming for class properties based on their type | | 🔧 | |
44
44
  | decorator-key-sort | Sorts the keys of the object passed to the `@Component/@Injectable/@NgModule/@Pipe` decorator | ✅ | 🔧 | |
45
45
  | flat-exports | Spread nested arrays when exporting Angular entity collections | | 🔧 | |
46
+ | host-attributes-sort | Sort Angular host metadata attributes using configurable attribute groups | ✅ | 🔧 | |
46
47
  | injection-token-description | They are required to provide a description for `InjectionToken` | ✅ | | |
47
48
  | no-deep-imports | Disables deep imports of Taiga UI packages | ✅ | 🔧 | |
48
49
  | no-deep-imports-to-indexed-packages | Disallow deep imports from packages that expose an index.ts next to ng-package.json or package.json | ✅ | 🔧 | |
@@ -54,9 +55,9 @@ export default [
54
55
  | no-project-as-in-ng-template | `ngProjectAs` has no effect inside `<ng-template>` or dynamic outlets | ✅ | | |
55
56
  | no-redundant-type-annotation | Disallow redundant type annotations when the type is already inferred from the initializer | ✅ | 🔧 | |
56
57
  | 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 | ✅ | | |
58
+ | no-signal-reads-after-await-in-reactive-context | Disallow bare signal reads after `await` inside reactive callbacks | ✅ | | |
58
59
  | no-string-literal-concat | Disallow string literal concatenation; merge adjacent literals into one | ✅ | 🔧 | |
59
- | no-untracked-outside-reactive-context | Disallow `untracked()` outside the synchronous body of reactive callbacks | ✅ | 🔧 | |
60
+ | no-untracked-outside-reactive-context | Disallow `untracked()` outside reactive callbacks, except explicit post-`await` snapshots | ✅ | 🔧 | |
60
61
  | no-useless-untracked | Disallow provably useless `untracked()` wrappers in reactive callbacks | ✅ | 🔧 | |
61
62
  | object-single-line | Enforce single-line formatting for single-property objects when it fits `printWidth` | ✅ | 🔧 | |
62
63
  | prefer-deep-imports | Allow deep imports of Taiga UI packages | | 🔧 | |
@@ -180,6 +181,105 @@ export const TuiInput = [...TuiTextfield, TuiInputDirective] as const;
180
181
 
181
182
  ---
182
183
 
184
+ ## host-attributes-sort
185
+
186
+ <sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
187
+
188
+ Sorts Angular `host` metadata entries in `@Component` and `@Directive` using configurable attribute groups, matching the
189
+ same grouping model used for template attributes in Prettier. The recommended config enables the rule with a default
190
+ group order that places `id` before plain attributes, `class`, animation bindings, inputs, two-way bindings, and
191
+ outputs.
192
+
193
+ ```ts
194
+ // ❌ error
195
+ @Component({
196
+ host: {
197
+ '(click)': 'handleClick()',
198
+ '[value]': 'value()',
199
+ class: 'cmp',
200
+ id: 'cmp-id',
201
+ },
202
+ })
203
+
204
+ // ✅ after autofix
205
+ @Component({
206
+ host: {
207
+ id: 'cmp-id',
208
+ class: 'cmp',
209
+ '[value]': 'value()',
210
+ '(click)': 'handleClick()',
211
+ },
212
+ })
213
+ ```
214
+
215
+ The rule understands the same preset names as `prettier-plugin-organize-attributes`. You can use aggregate presets such
216
+ as `$ANGULAR`, `$HTML`, and `$CODE_GUIDE`, or compose atomic presets such as `$CLASS`, `$ID`, `$ARIA`, `$ANGULAR_INPUT`,
217
+ `$ANGULAR_TWO_WAY_BINDING`, and `$ANGULAR_OUTPUT`.
218
+
219
+ ```json
220
+ {
221
+ "@taiga-ui/experience-next/host-attributes-sort": [
222
+ "error",
223
+ {
224
+ "attributeGroups": ["$ANGULAR"]
225
+ }
226
+ ]
227
+ }
228
+ ```
229
+
230
+ Use `$ANGULAR` when `host` should follow the familiar Angular template-style order:
231
+ `class -> id -> #ref -> *directive -> @animation -> [@animation] -> [(model)] -> [input] -> (output)`.
232
+
233
+ ```json
234
+ {
235
+ "@taiga-ui/experience-next/host-attributes-sort": [
236
+ "error",
237
+ {
238
+ "attributeGroups": ["$HTML"]
239
+ }
240
+ ]
241
+ }
242
+ ```
243
+
244
+ Use `$HTML` when only `class` and `id` should be pulled to the front, and everything else can stay in the trailing
245
+ default group.
246
+
247
+ ```json
248
+ {
249
+ "@taiga-ui/experience-next/host-attributes-sort": [
250
+ "error",
251
+ {
252
+ "attributeGroups": ["$CODE_GUIDE"]
253
+ }
254
+ ]
255
+ }
256
+ ```
257
+
258
+ Use `$CODE_GUIDE` for a wider HTML-oriented order: `class`, `id`, `name`, `data-*`, `src`, `for`, `type`, `href`,
259
+ `value`, `title`, `alt`, `role`, `aria-*`.
260
+
261
+ ```json
262
+ {
263
+ "@taiga-ui/experience-next/host-attributes-sort": [
264
+ "error",
265
+ {
266
+ "attributeGroups": ["$ID", "$DEFAULT", "$ARIA", "$ANGULAR_OUTPUT"]
267
+ }
268
+ ]
269
+ }
270
+ ```
271
+
272
+ Use atomic presets when you want a custom order instead of one of the bundled aliases.
273
+
274
+ | Option | Type | Description |
275
+ | --------------------- | --------------------------- | ----------------------------------------------------------------- |
276
+ | `attributeGroups` | `string[]` | Group order. Supports the same preset tokens as Prettier plugins. |
277
+ | `attributeIgnoreCase` | `boolean` | Ignore case when matching custom regexp groups. |
278
+ | `attributeSort` | `'ASC' \| 'DESC' \| 'NONE'` | Sort order inside each matched group. |
279
+ | `decorators` | `string[]` | Decorator names whose `host` metadata should be checked. |
280
+
281
+ ---
282
+
183
283
  ## injection-token-description
184
284
 
185
285
  <sup>`✅ Recommended`</sup>
@@ -602,8 +702,9 @@ const derived = computed(() => source() + 1);
602
702
 
603
703
  <sup>`✅ Recommended`</sup>
604
704
 
605
- Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any signal read
606
- after `await` will not become a dependency.
705
+ Angular tracks signal reads only in synchronous code. If a reactive callback crosses an async boundary, any bare signal
706
+ read after `await` will not become a dependency. Snapshot before `await` when you need the earlier value, or make an
707
+ intentional post-`await` current-value read explicit with `untracked(...)`.
607
708
 
608
709
  ```ts
609
710
  // ❌ error
@@ -620,6 +721,14 @@ effect(async () => {
620
721
  });
621
722
  ```
622
723
 
724
+ ```ts
725
+ // ✅ ok — explicit current-value read after await
726
+ effect(async () => {
727
+ await this.fetchUser();
728
+ console.log(untracked(this.theme));
729
+ });
730
+ ```
731
+
623
732
  ---
624
733
 
625
734
  ## no-untracked-outside-reactive-context
@@ -627,14 +736,15 @@ effect(async () => {
627
736
  <sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
628
737
 
629
738
  `untracked()` usually only affects signal reads that happen inside the synchronous body of a reactive callback. In
630
- ordinary non-reactive code, nested callbacks, or code that runs after `await`, it usually does not prevent dependency
631
- tracking and only adds noise. This rule reports those cases, but intentionally allows a few imperative Angular escape
632
- hatches where `untracked()` can still be useful: `@Pipe().transform`, `ControlValueAccessor.writeValue`,
633
- `registerOnChange` including patched accessors such as `accessor.writeValue = (...) => {}`, callback-form wrappers used
634
- inside deferred scheduler / event-handler callbacks, and narrow lazy DI factory wrappers like
635
- `InjectionToken({factory})` / `useFactory` when they guard creation of a reactive owner such as `effect()` against an
636
- accidental ambient reactive context. For the narrow case `untracked(() => effect(...))` and similar outer wrappers
637
- around a reactive call in ordinary code, autofix removes only the useless outer `untracked()` wrapper.
739
+ ordinary non-reactive code or nested callbacks it usually does not prevent dependency tracking and only adds noise. This
740
+ rule reports those cases, but intentionally allows a few explicit escape hatches: post-`await` reads inside a reactive
741
+ callback when `untracked()` is used to document an intentional current-value snapshot, imperative Angular hooks such as
742
+ `@Pipe().transform`, `ControlValueAccessor.writeValue`, `registerOnChange` including patched accessors such as
743
+ `accessor.writeValue = (...) => {}`, callback-form wrappers used inside deferred scheduler / event-handler callbacks,
744
+ and narrow lazy DI factory wrappers like `InjectionToken({factory})` / `useFactory` when they guard creation of a
745
+ reactive owner such as `effect()` against an accidental ambient reactive context. For the narrow case
746
+ `untracked(() => effect(...))` and similar outer wrappers around a reactive call in ordinary code, autofix removes only
747
+ the useless outer `untracked()` wrapper.
638
748
 
639
749
  ```ts
640
750
  // ❌ error
@@ -656,6 +766,17 @@ effect(() => {
656
766
  const snapshot = computed(() => untracked(this.user));
657
767
  ```
658
768
 
769
+ ```ts
770
+ // ✅ ok — after await, untracked can mark an intentional current snapshot
771
+ effect(async () => {
772
+ await this.refresh();
773
+
774
+ if (untracked(this.user) !== previousUser) {
775
+ console.log('changed');
776
+ }
777
+ });
778
+ ```
779
+
659
780
  ```ts
660
781
  // ❌ error
661
782
  untracked(() => {
package/index.d.ts CHANGED
@@ -18,6 +18,14 @@ declare const plugin: {
18
18
  'flat-exports': import("@typescript-eslint/utils/ts-eslint").RuleModule<"spreadArrays", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
19
19
  name: string;
20
20
  };
21
+ 'host-attributes-sort': import("@typescript-eslint/utils/ts-eslint").RuleModule<"incorrectOrder", [{
22
+ attributeGroups?: string[];
23
+ attributeIgnoreCase?: boolean;
24
+ attributeSort?: "ASC" | "DESC" | "NONE";
25
+ decorators?: string[];
26
+ }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
27
+ name: string;
28
+ };
21
29
  'html-logical-properties': import("eslint").Rule.RuleModule;
22
30
  'injection-token-description': import("@typescript-eslint/utils/ts-eslint").RuleModule<"invalid-injection-token-description", readonly unknown[], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
23
31
  name: string;
package/index.esm.js CHANGED
@@ -896,6 +896,7 @@ var recommended = defineConfig([
896
896
  Pipe: ['standalone', 'name', 'pure'],
897
897
  },
898
898
  ],
899
+ '@taiga-ui/experience-next/host-attributes-sort': 'error',
899
900
  '@taiga-ui/experience-next/injection-token-description': 'error',
900
901
  '@taiga-ui/experience-next/no-deep-imports': [
901
902
  'error',
@@ -1334,8 +1335,8 @@ function intersect(a, b) {
1334
1335
  return a.some((type) => origin.has(type));
1335
1336
  }
1336
1337
 
1337
- const createRule$g = ESLintUtils.RuleCreator((name) => name);
1338
- var classPropertyNaming = createRule$g({
1338
+ const createRule$h = ESLintUtils.RuleCreator((name) => name);
1339
+ var classPropertyNaming = createRule$h({
1339
1340
  create(context, [configs]) {
1340
1341
  const parserServices = ESLintUtils.getParserServices(context);
1341
1342
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1504,9 +1505,9 @@ function isExternalPureTuple(typeChecker, type) {
1504
1505
  return typeArgs.every((item) => isClassType(item));
1505
1506
  }
1506
1507
 
1507
- const createRule$f = ESLintUtils.RuleCreator((name) => name);
1508
+ const createRule$g = ESLintUtils.RuleCreator((name) => name);
1508
1509
  const MESSAGE_ID$7 = 'spreadArrays';
1509
- var flatExports = createRule$f({
1510
+ var flatExports = createRule$g({
1510
1511
  create(context) {
1511
1512
  const parserServices = ESLintUtils.getParserServices(context);
1512
1513
  const typeChecker = parserServices.program.getTypeChecker();
@@ -1630,6 +1631,317 @@ var flatExports = createRule$f({
1630
1631
  name: 'flat-exports',
1631
1632
  });
1632
1633
 
1634
+ function isObject(node) {
1635
+ return node?.type === AST_NODE_TYPES$1.ObjectExpression;
1636
+ }
1637
+
1638
+ /**
1639
+ * Extracts the metadata object from a class decorator such as
1640
+ * `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
1641
+ *
1642
+ * Returns the first argument of the decorator call *if and only if*
1643
+ * it is an `ObjectExpression`.
1644
+ *
1645
+ * @example
1646
+ * // Given:
1647
+ * @Component({
1648
+ * selector: 'x',
1649
+ * imports: [A, B],
1650
+ * })
1651
+ * class MyCmp {}
1652
+ *
1653
+ * // In the AST for @Component(...)
1654
+ * getDecoratorMetadata(decorator, allowed) →
1655
+ * ObjectExpression({ selector: ..., imports: ... })
1656
+ *
1657
+ * @param decorator - The decorator node attached to a class declaration.
1658
+ * @param allowedNames - A set of decorator names to consider
1659
+ * (e.g., Component, Directive, NgModule, Pipe).
1660
+ *
1661
+ * @returns The metadata `ObjectExpression` if present and valid,
1662
+ * otherwise `null`.
1663
+ */
1664
+ function getDecoratorMetadata(decorator, allowedNames) {
1665
+ const expr = decorator.expression;
1666
+ if (expr.type !== AST_NODE_TYPES$1.CallExpression) {
1667
+ return null;
1668
+ }
1669
+ const callee = expr.callee;
1670
+ if (callee.type !== AST_NODE_TYPES$1.Identifier) {
1671
+ return null;
1672
+ }
1673
+ if (!allowedNames.has(callee.name)) {
1674
+ return null;
1675
+ }
1676
+ const arg = expr.arguments[0];
1677
+ return isObject(arg) ? arg : null;
1678
+ }
1679
+
1680
+ function sameOrder(a, b) {
1681
+ return a.length === b.length && a.every((value, index) => value === b[index]);
1682
+ }
1683
+
1684
+ const DEFAULT_GROUP = '$DEFAULT';
1685
+ const DEFAULT_ATTRIBUTE_GROUPS = [
1686
+ '$ANGULAR_STRUCTURAL_DIRECTIVE',
1687
+ '$ANGULAR_ELEMENT_REF',
1688
+ '$ID',
1689
+ '$DEFAULT',
1690
+ '$CLASS',
1691
+ '$ANGULAR_ANIMATION',
1692
+ '$ANGULAR_ANIMATION_INPUT',
1693
+ '$ANGULAR_INPUT',
1694
+ '$ANGULAR_TWO_WAY_BINDING',
1695
+ '$ANGULAR_OUTPUT',
1696
+ ];
1697
+ const DEFAULT_DECORATORS$1 = ['Component', 'Directive'];
1698
+ const PRESETS = {
1699
+ $ALT: /^alt$/,
1700
+ $ANGULAR: [
1701
+ '$CLASS',
1702
+ '$ID',
1703
+ '$ANGULAR_ELEMENT_REF',
1704
+ '$ANGULAR_STRUCTURAL_DIRECTIVE',
1705
+ '$ANGULAR_ANIMATION',
1706
+ '$ANGULAR_ANIMATION_INPUT',
1707
+ '$ANGULAR_TWO_WAY_BINDING',
1708
+ '$ANGULAR_INPUT',
1709
+ '$ANGULAR_OUTPUT',
1710
+ ],
1711
+ $ANGULAR_ANIMATION: /^@/,
1712
+ $ANGULAR_ANIMATION_INPUT: /^\[@/,
1713
+ $ANGULAR_ELEMENT_REF: /^#/,
1714
+ $ANGULAR_INPUT: /^\[[^(@]/,
1715
+ $ANGULAR_OUTPUT: /^\(/,
1716
+ $ANGULAR_STRUCTURAL_DIRECTIVE: /^\*/,
1717
+ $ANGULAR_TWO_WAY_BINDING: /^\[\(/,
1718
+ $ARIA: /^aria-/,
1719
+ $CLASS: /^class$/,
1720
+ $CODE_GUIDE: [
1721
+ '$CLASS',
1722
+ '$ID',
1723
+ '$NAME',
1724
+ '$DATA',
1725
+ '$SRC',
1726
+ '$FOR',
1727
+ '$TYPE',
1728
+ '$HREF',
1729
+ '$VALUE',
1730
+ '$TITLE',
1731
+ '$ALT',
1732
+ '$ROLE',
1733
+ '$ARIA',
1734
+ ],
1735
+ $DATA: /^data-/,
1736
+ $FOR: /^for$/,
1737
+ $HREF: /^href$/,
1738
+ $HTML: ['$CLASS', '$ID'],
1739
+ $ID: /^id$/,
1740
+ $NAME: /^name$/,
1741
+ $ROLE: /^role$/,
1742
+ $SRC: /^src$/,
1743
+ $TITLE: /^title$/,
1744
+ $TYPE: /^type$/,
1745
+ $VALUE: /^value$/,
1746
+ $VUE: ['$CLASS', '$ID', '$VUE_ATTRIBUTE'],
1747
+ $VUE_ATTRIBUTE: /^v-/,
1748
+ };
1749
+ const createRule$f = ESLintUtils.RuleCreator((name) => name);
1750
+ const rule$i = createRule$f({
1751
+ create(context, [options]) {
1752
+ const sourceCode = context.sourceCode;
1753
+ const settings = {
1754
+ attributeGroups: [...DEFAULT_ATTRIBUTE_GROUPS],
1755
+ attributeIgnoreCase: true,
1756
+ attributeSort: 'ASC',
1757
+ decorators: [...DEFAULT_DECORATORS$1],
1758
+ ...options,
1759
+ };
1760
+ const allowedDecorators = new Set(settings.decorators);
1761
+ return {
1762
+ ClassDeclaration(node) {
1763
+ for (const decorator of node?.decorators ?? []) {
1764
+ const metadata = getDecoratorMetadata(decorator, allowedDecorators);
1765
+ if (!metadata) {
1766
+ continue;
1767
+ }
1768
+ const hostObject = getHostObject(metadata);
1769
+ if (!hostObject) {
1770
+ continue;
1771
+ }
1772
+ const properties = getHostAttributeProperties(hostObject);
1773
+ if (!properties || properties.length <= 1) {
1774
+ continue;
1775
+ }
1776
+ const sortedProperties = organizeProperties(properties, settings);
1777
+ const currentOrder = properties.map(({ name }) => name);
1778
+ const expectedOrder = sortedProperties.map(({ name }) => name);
1779
+ if (sameOrder(currentOrder, expectedOrder)) {
1780
+ continue;
1781
+ }
1782
+ const report = {
1783
+ data: { expected: expectedOrder.join(', ') },
1784
+ messageId: 'incorrectOrder',
1785
+ node: hostObject,
1786
+ };
1787
+ if (sourceCode.getCommentsInside(hostObject).length > 0) {
1788
+ context.report(report);
1789
+ continue;
1790
+ }
1791
+ context.report({
1792
+ ...report,
1793
+ fix: (fixer) => fixer.replaceTextRange(hostObject.range, `{${sortedProperties
1794
+ .map(({ node: property }) => sourceCode.getText(property))
1795
+ .join(', ')}}`),
1796
+ });
1797
+ }
1798
+ },
1799
+ };
1800
+ },
1801
+ meta: {
1802
+ defaultOptions: [
1803
+ {
1804
+ attributeGroups: [...DEFAULT_ATTRIBUTE_GROUPS],
1805
+ attributeIgnoreCase: true,
1806
+ attributeSort: 'ASC',
1807
+ decorators: [...DEFAULT_DECORATORS$1],
1808
+ },
1809
+ ],
1810
+ docs: {
1811
+ description: 'Sort Angular host metadata attributes using configurable attribute groups.',
1812
+ },
1813
+ fixable: 'code',
1814
+ messages: { incorrectOrder: 'Host attributes should be sorted as [{{expected}}]' },
1815
+ schema: [
1816
+ {
1817
+ additionalProperties: false,
1818
+ properties: {
1819
+ attributeGroups: {
1820
+ items: { type: 'string' },
1821
+ type: 'array',
1822
+ },
1823
+ attributeIgnoreCase: { type: 'boolean' },
1824
+ attributeSort: {
1825
+ enum: ['ASC', 'DESC', 'NONE'],
1826
+ type: 'string',
1827
+ },
1828
+ decorators: {
1829
+ items: { type: 'string' },
1830
+ type: 'array',
1831
+ },
1832
+ },
1833
+ type: 'object',
1834
+ },
1835
+ ],
1836
+ type: 'problem',
1837
+ },
1838
+ name: 'host-attributes-sort',
1839
+ });
1840
+ function getHostObject(metadata) {
1841
+ for (const property of metadata.properties) {
1842
+ if (property.type !== AST_NODE_TYPES$1.Property ||
1843
+ property.kind !== 'init' ||
1844
+ property.computed ||
1845
+ property.method) {
1846
+ continue;
1847
+ }
1848
+ if (getStaticPropertyName(property.key) !== 'host') {
1849
+ continue;
1850
+ }
1851
+ return property.value.type === AST_NODE_TYPES$1.ObjectExpression
1852
+ ? property.value
1853
+ : null;
1854
+ }
1855
+ return null;
1856
+ }
1857
+ function getHostAttributeProperties(hostObject) {
1858
+ const properties = [];
1859
+ for (const property of hostObject.properties) {
1860
+ if (property.type !== AST_NODE_TYPES$1.Property ||
1861
+ property.kind !== 'init' ||
1862
+ property.computed ||
1863
+ property.method) {
1864
+ return null;
1865
+ }
1866
+ const name = getStaticPropertyName(property.key);
1867
+ if (name === null) {
1868
+ return null;
1869
+ }
1870
+ properties.push({ name, node: property });
1871
+ }
1872
+ return properties;
1873
+ }
1874
+ function getStaticPropertyName(key) {
1875
+ if (key.type === AST_NODE_TYPES$1.Identifier) {
1876
+ return key.name;
1877
+ }
1878
+ if (key.type === AST_NODE_TYPES$1.Literal &&
1879
+ (typeof key.value === 'string' || typeof key.value === 'number')) {
1880
+ return String(key.value);
1881
+ }
1882
+ if (key.type === AST_NODE_TYPES$1.TemplateLiteral &&
1883
+ key.expressions.length === 0 &&
1884
+ key.quasis.length === 1) {
1885
+ return key.quasis[0]?.value.cooked ?? null;
1886
+ }
1887
+ return null;
1888
+ }
1889
+ function organizeProperties(properties, options) {
1890
+ const groups = getGroups(options.attributeGroups.length > 0 ? options.attributeGroups : ['$ANGULAR'], options.attributeIgnoreCase);
1891
+ const defaultGroup = ensureDefaultGroup(groups);
1892
+ for (const property of properties) {
1893
+ const targetGroup = groups.find(({ regexp }) => regexp?.test(property.name)) ?? defaultGroup;
1894
+ targetGroup.values.push(property);
1895
+ }
1896
+ if (options.attributeSort !== 'NONE') {
1897
+ for (const group of groups) {
1898
+ group.values.sort((left, right) => left.name.localeCompare(right.name));
1899
+ if (options.attributeSort === 'DESC') {
1900
+ group.values.reverse();
1901
+ }
1902
+ }
1903
+ }
1904
+ return groups.flatMap(({ values }) => values);
1905
+ }
1906
+ function getGroups(queries, ignoreCase) {
1907
+ return queries.flatMap((query) => getGroup(query, ignoreCase));
1908
+ }
1909
+ function getGroup(query, ignoreCase) {
1910
+ if (query === DEFAULT_GROUP) {
1911
+ return [createDefaultGroup()];
1912
+ }
1913
+ const preset = PRESETS[query];
1914
+ if (!preset) {
1915
+ return [
1916
+ {
1917
+ query,
1918
+ regexp: new RegExp(query, ignoreCase ? 'i' : ''),
1919
+ values: [],
1920
+ },
1921
+ ];
1922
+ }
1923
+ if (Array.isArray(preset)) {
1924
+ return preset.flatMap((item) => getGroup(item, ignoreCase));
1925
+ }
1926
+ return [{ query, regexp: preset, values: [] }];
1927
+ }
1928
+ function ensureDefaultGroup(groups) {
1929
+ const existing = groups.find(({ unknown }) => unknown);
1930
+ if (existing) {
1931
+ return existing;
1932
+ }
1933
+ const fallback = createDefaultGroup();
1934
+ groups.push(fallback);
1935
+ return fallback;
1936
+ }
1937
+ function createDefaultGroup() {
1938
+ return {
1939
+ query: DEFAULT_GROUP,
1940
+ unknown: true,
1941
+ values: [],
1942
+ };
1943
+ }
1944
+
1633
1945
  const DIRECTIONAL_TO_LOGICAL = {
1634
1946
  'border-bottom': 'border-block-end',
1635
1947
  'border-left': 'border-inline-start',
@@ -2397,6 +2709,16 @@ function isNodeInsideSynchronousReactiveScope(node, callback) {
2397
2709
  });
2398
2710
  return found;
2399
2711
  }
2712
+ function isNodeAfterAsyncBoundaryInReactiveScope(node, callback) {
2713
+ let found = false;
2714
+ walkAfterAsyncBoundaryAst(callback, (inner) => {
2715
+ if (inner !== node) {
2716
+ return;
2717
+ }
2718
+ found = true;
2719
+ });
2720
+ return found;
2721
+ }
2400
2722
  function findEnclosingReactiveScope(node, program) {
2401
2723
  for (let current = node.parent; current; current = current.parent) {
2402
2724
  if (current.type !== AST_NODE_TYPES.CallExpression) {
@@ -2410,6 +2732,19 @@ function findEnclosingReactiveScope(node, program) {
2410
2732
  }
2411
2733
  return null;
2412
2734
  }
2735
+ function findEnclosingReactiveScopeAfterAsyncBoundary(node, program) {
2736
+ for (let current = node.parent; current; current = current.parent) {
2737
+ if (current.type !== AST_NODE_TYPES.CallExpression) {
2738
+ continue;
2739
+ }
2740
+ for (const scope of getReactiveScopes(current, program)) {
2741
+ if (isNodeAfterAsyncBoundaryInReactiveScope(node, scope.callback)) {
2742
+ return scope;
2743
+ }
2744
+ }
2745
+ }
2746
+ return null;
2747
+ }
2413
2748
  /**
2414
2749
  * Returns true when the TypeScript type at `node` is an Angular signal type.
2415
2750
  * Uses duck-typing: callable type whose name contains "Signal", or whose
@@ -3256,6 +3591,7 @@ const rule$9 = createUntrackedRule({
3256
3591
  const reported = new Set();
3257
3592
  walkAfterAsyncBoundaryAst(scope.callback, (inner) => {
3258
3593
  if (inner.type !== AST_NODE_TYPES.CallExpression ||
3594
+ isAngularUntrackedCall(inner, program) ||
3259
3595
  !isSignalReadCall(inner, checker, esTreeNodeToTSNodeMap)) {
3260
3596
  return;
3261
3597
  }
@@ -3279,11 +3615,11 @@ const rule$9 = createUntrackedRule({
3279
3615
  },
3280
3616
  meta: {
3281
3617
  docs: {
3282
- description: 'Disallow signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
3618
+ description: 'Disallow bare signal reads that occur after `await` inside reactive callbacks, because Angular no longer tracks them as dependencies',
3283
3619
  url: ANGULAR_SIGNALS_ASYNC_GUIDE_URL,
3284
3620
  },
3285
3621
  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',
3622
+ 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
3623
  },
3288
3624
  schema: [],
3289
3625
  type: 'problem',
@@ -3802,7 +4138,8 @@ const rule$7 = createUntrackedRule({
3802
4138
  if (!isAngularUntrackedCall(node, program)) {
3803
4139
  return;
3804
4140
  }
3805
- if (findEnclosingReactiveScope(node, program)) {
4141
+ if (findEnclosingReactiveScope(node, program) ||
4142
+ findEnclosingReactiveScopeAfterAsyncBoundary(node, program)) {
3806
4143
  return;
3807
4144
  }
3808
4145
  if (isAllowedImperativeAngularContext(node)) {
@@ -3840,12 +4177,12 @@ const rule$7 = createUntrackedRule({
3840
4177
  },
3841
4178
  meta: {
3842
4179
  docs: {
3843
- description: 'Disallow `untracked()` outside the synchronous body of a reactive callback, except for supported imperative Angular hooks, deferred callback wrappers, and lazy DI factories that may execute under an ambient reactive context',
4180
+ 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
4181
  url: ANGULAR_SIGNALS_UNTRACKED_GUIDE_URL,
3845
4182
  },
3846
4183
  fixable: 'code',
3847
4184
  messages: {
3848
- outsideReactiveContext: '`untracked()` is used outside the synchronous body of a reactive callback and outside the supported 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',
4185
+ 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
4186
  },
3850
4187
  schema: [],
3851
4188
  type: 'problem',
@@ -5290,52 +5627,6 @@ const rule$1 = createRule$2({
5290
5627
  name: 'short-tui-imports',
5291
5628
  });
5292
5629
 
5293
- function isObject(node) {
5294
- return node?.type === AST_NODE_TYPES$1.ObjectExpression;
5295
- }
5296
-
5297
- /**
5298
- * Extracts the metadata object from a class decorator such as
5299
- * `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
5300
- *
5301
- * Returns the first argument of the decorator call *if and only if*
5302
- * it is an `ObjectExpression`.
5303
- *
5304
- * @example
5305
- * // Given:
5306
- * @Component({
5307
- * selector: 'x',
5308
- * imports: [A, B],
5309
- * })
5310
- * class MyCmp {}
5311
- *
5312
- * // In the AST for @Component(...)
5313
- * getDecoratorMetadata(decorator, allowed) →
5314
- * ObjectExpression({ selector: ..., imports: ... })
5315
- *
5316
- * @param decorator - The decorator node attached to a class declaration.
5317
- * @param allowedNames - A set of decorator names to consider
5318
- * (e.g., Component, Directive, NgModule, Pipe).
5319
- *
5320
- * @returns The metadata `ObjectExpression` if present and valid,
5321
- * otherwise `null`.
5322
- */
5323
- function getDecoratorMetadata(decorator, allowedNames) {
5324
- const expr = decorator.expression;
5325
- if (expr.type !== AST_NODE_TYPES$1.CallExpression) {
5326
- return null;
5327
- }
5328
- const callee = expr.callee;
5329
- if (callee.type !== AST_NODE_TYPES$1.Identifier) {
5330
- return null;
5331
- }
5332
- if (!allowedNames.has(callee.name)) {
5333
- return null;
5334
- }
5335
- const arg = expr.arguments[0];
5336
- return isObject(arg) ? arg : null;
5337
- }
5338
-
5339
5630
  function getImportsArray(meta) {
5340
5631
  const property = meta.properties.find((literal) => literal.type === AST_NODE_TYPES$1.Property &&
5341
5632
  literal.key.type === AST_NODE_TYPES$1.Identifier &&
@@ -5411,10 +5702,6 @@ function getSortedNames(elements, source) {
5411
5702
  return [...sortedRegular, ...sortedSpreads].map((n) => nameOf(n, source));
5412
5703
  }
5413
5704
 
5414
- function sameOrder(a, b) {
5415
- return a.length === b.length && a.every((value, index) => value === b[index]);
5416
- }
5417
-
5418
5705
  const createRule$1 = ESLintUtils.RuleCreator((name) => name);
5419
5706
  var standaloneImportsSort = createRule$1({
5420
5707
  create(context, [options]) {
@@ -5584,6 +5871,7 @@ const plugin = {
5584
5871
  'class-property-naming': classPropertyNaming,
5585
5872
  'decorator-key-sort': config$3,
5586
5873
  'flat-exports': flatExports,
5874
+ 'host-attributes-sort': rule$i,
5587
5875
  'html-logical-properties': config$2,
5588
5876
  'injection-token-description': rule$h,
5589
5877
  'no-deep-imports': rule$g,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taiga-ui/eslint-plugin-experience-next",
3
- "version": "0.469.0",
3
+ "version": "0.471.0",
4
4
  "description": "An ESLint plugin to enforce a consistent code styles across taiga-ui projects",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,14 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ type SortOrder = 'ASC' | 'DESC' | 'NONE';
3
+ type Options = [
4
+ {
5
+ attributeGroups?: string[];
6
+ attributeIgnoreCase?: boolean;
7
+ attributeSort?: SortOrder;
8
+ decorators?: string[];
9
+ }
10
+ ];
11
+ export declare const rule: ESLintUtils.RuleModule<"incorrectOrder", Options, unknown, ESLintUtils.RuleListener> & {
12
+ name: string;
13
+ };
14
+ export default rule;
@@ -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