@taiga-ui/eslint-plugin-experience-next 0.470.0 → 0.472.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 +115 -7
- package/index.d.ts +8 -0
- package/index.esm.js +410 -75
- package/package.json +1 -1
- package/rules/host-attributes-sort.d.ts +14 -0
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ 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
|
-
|
|
|
46
|
+
| host-attributes-sort | Sort Angular host metadata attributes using configurable attribute groups | ✅ | 🔧 | |
|
|
47
|
+
| injection-token-description | Require `InjectionToken` descriptions to include the token name | ✅ | 🔧 | |
|
|
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 | ✅ | 🔧 | |
|
|
49
50
|
| no-fully-untracked-effect | Disallow reactive callbacks where all signal reads are hidden inside `untracked()` | ✅ | | |
|
|
@@ -180,19 +181,126 @@ 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
|
-
<sup>`✅ Recommended`</sup>
|
|
285
|
+
<sup>`✅ Recommended`</sup> <sup>`Fixable`</sup>
|
|
186
286
|
|
|
187
|
-
The description
|
|
188
|
-
|
|
287
|
+
The description passed to `new InjectionToken(...)` must contain the name of the variable it is assigned to. The rule
|
|
288
|
+
accepts both direct string descriptions and Angular's `ngDevMode ? '...' : ''` pattern, and the autofix rewrites invalid
|
|
289
|
+
descriptions to the dev-only form. If `ngDevMode` is not declared in the file, the autofix inserts
|
|
290
|
+
`declare const ngDevMode: boolean;` after imports.
|
|
189
291
|
|
|
190
292
|
```ts
|
|
191
|
-
// ❌ error
|
|
192
|
-
|
|
293
|
+
// ❌ error
|
|
294
|
+
import {InjectionToken} from '@angular/core';
|
|
295
|
+
|
|
296
|
+
export const TUI_MY_TOKEN = new InjectionToken<string>('some description');
|
|
193
297
|
|
|
194
298
|
// ✅ after autofix
|
|
195
|
-
|
|
299
|
+
import {InjectionToken} from '@angular/core';
|
|
300
|
+
|
|
301
|
+
declare const ngDevMode: boolean;
|
|
302
|
+
|
|
303
|
+
export const TUI_MY_TOKEN = new InjectionToken<string>(ngDevMode ? '[TUI_MY_TOKEN]: some description' : '');
|
|
196
304
|
```
|
|
197
305
|
|
|
198
306
|
---
|
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$
|
|
1338
|
-
var classPropertyNaming = createRule$
|
|
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$
|
|
1508
|
+
const createRule$g = ESLintUtils.RuleCreator((name) => name);
|
|
1508
1509
|
const MESSAGE_ID$7 = 'spreadArrays';
|
|
1509
|
-
var flatExports = createRule$
|
|
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',
|
|
@@ -1712,37 +2024,109 @@ const config$2 = {
|
|
|
1712
2024
|
|
|
1713
2025
|
const MESSAGE_ID$5 = 'invalid-injection-token-description';
|
|
1714
2026
|
const ERROR_MESSAGE$3 = "InjectionToken's description should contain token's name";
|
|
2027
|
+
const NG_DEV_MODE = 'ngDevMode';
|
|
1715
2028
|
const createRule$e = ESLintUtils.RuleCreator((name) => name);
|
|
2029
|
+
function getVariableName(node) {
|
|
2030
|
+
if (node.parent.type !== AST_NODE_TYPES$1.VariableDeclarator) {
|
|
2031
|
+
return undefined;
|
|
2032
|
+
}
|
|
2033
|
+
const { id } = node.parent;
|
|
2034
|
+
return id.type === AST_NODE_TYPES$1.Identifier ? id.name : undefined;
|
|
2035
|
+
}
|
|
2036
|
+
function isStringLiteral$1(node) {
|
|
2037
|
+
return node.type === AST_NODE_TYPES$1.Literal && typeof node.value === 'string';
|
|
2038
|
+
}
|
|
2039
|
+
function isStringLike(node) {
|
|
2040
|
+
return isStringLiteral$1(node) || node.type === AST_NODE_TYPES$1.TemplateLiteral;
|
|
2041
|
+
}
|
|
2042
|
+
function getStringValue(node) {
|
|
2043
|
+
if (isStringLiteral$1(node)) {
|
|
2044
|
+
return node.value;
|
|
2045
|
+
}
|
|
2046
|
+
return node.quasis[0]?.value.raw || '';
|
|
2047
|
+
}
|
|
2048
|
+
function isEmptyString$1(node) {
|
|
2049
|
+
return (getStringValue(node) === '' &&
|
|
2050
|
+
(!('expressions' in node) || !node.expressions.length));
|
|
2051
|
+
}
|
|
2052
|
+
function isNgDevModeConditional(node) {
|
|
2053
|
+
return (node.type === AST_NODE_TYPES$1.ConditionalExpression &&
|
|
2054
|
+
node.test.type === AST_NODE_TYPES$1.Identifier &&
|
|
2055
|
+
node.test.name === NG_DEV_MODE &&
|
|
2056
|
+
isStringLike(node.consequent) &&
|
|
2057
|
+
isStringLike(node.alternate) &&
|
|
2058
|
+
isEmptyString$1(node.alternate));
|
|
2059
|
+
}
|
|
2060
|
+
function getDescriptionValue(node) {
|
|
2061
|
+
if (isStringLike(node)) {
|
|
2062
|
+
return getStringValue(node);
|
|
2063
|
+
}
|
|
2064
|
+
if (isNgDevModeConditional(node)) {
|
|
2065
|
+
return getStringValue(node.consequent);
|
|
2066
|
+
}
|
|
2067
|
+
return undefined;
|
|
2068
|
+
}
|
|
2069
|
+
function getDescriptionNode(node) {
|
|
2070
|
+
if (isStringLike(node)) {
|
|
2071
|
+
return node;
|
|
2072
|
+
}
|
|
2073
|
+
return isNgDevModeConditional(node) ? node.consequent : undefined;
|
|
2074
|
+
}
|
|
2075
|
+
function prependTokenName(text, name) {
|
|
2076
|
+
return `${text.slice(0, 1)}[${name}]: ${text.slice(1)}`;
|
|
2077
|
+
}
|
|
2078
|
+
function isNgDevModeVisible(sourceCode, node) {
|
|
2079
|
+
for (let scope = sourceCode.getScope(node); scope !== null; scope = scope.upper) {
|
|
2080
|
+
if (scope.variables.some((variable) => variable.name === NG_DEV_MODE)) {
|
|
2081
|
+
return true;
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return false;
|
|
2085
|
+
}
|
|
2086
|
+
function getNgDevModeDeclarationFix(program, fixer) {
|
|
2087
|
+
const lastImport = [...program.body]
|
|
2088
|
+
.reverse()
|
|
2089
|
+
.find((statement) => statement.type === AST_NODE_TYPES$1.ImportDeclaration);
|
|
2090
|
+
if (lastImport) {
|
|
2091
|
+
return fixer.insertTextAfter(lastImport, '\n\ndeclare const ngDevMode: boolean;');
|
|
2092
|
+
}
|
|
2093
|
+
const [firstStatement] = program.body;
|
|
2094
|
+
if (firstStatement) {
|
|
2095
|
+
return fixer.insertTextBefore(firstStatement, 'declare const ngDevMode: boolean;\n\n');
|
|
2096
|
+
}
|
|
2097
|
+
return fixer.insertTextBeforeRange([0, 0], 'declare const ngDevMode: boolean;\n');
|
|
2098
|
+
}
|
|
1716
2099
|
const rule$h = createRule$e({
|
|
1717
2100
|
create(context) {
|
|
2101
|
+
const { sourceCode } = context;
|
|
2102
|
+
const program = sourceCode.ast;
|
|
2103
|
+
let shouldAddNgDevModeDeclaration = true;
|
|
1718
2104
|
return {
|
|
1719
2105
|
'NewExpression[callee.name="InjectionToken"]'(node) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
const [description] = node?.arguments ?? [];
|
|
1723
|
-
if (!description) {
|
|
2106
|
+
const [description] = node.arguments;
|
|
2107
|
+
if (!description || description.type === AST_NODE_TYPES$1.SpreadElement) {
|
|
1724
2108
|
return;
|
|
1725
2109
|
}
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
token = description.value;
|
|
1730
|
-
}
|
|
1731
|
-
if (description.type === AST_NODE_TYPES$1.TemplateLiteral) {
|
|
1732
|
-
token = description.quasis[0]?.value.raw || '';
|
|
1733
|
-
}
|
|
1734
|
-
if (node?.parent.type === AST_NODE_TYPES$1.VariableDeclarator) {
|
|
1735
|
-
const id = node.parent.id;
|
|
1736
|
-
if (id.type === AST_NODE_TYPES$1.Identifier) {
|
|
1737
|
-
name = id.name;
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
2110
|
+
const name = getVariableName(node);
|
|
2111
|
+
const token = getDescriptionValue(description);
|
|
2112
|
+
const fixedDescription = getDescriptionNode(description);
|
|
1740
2113
|
const report = name && token && !token.includes(name);
|
|
1741
|
-
if (report) {
|
|
2114
|
+
if (report && fixedDescription) {
|
|
1742
2115
|
context.report({
|
|
1743
2116
|
fix: (fixer) => {
|
|
1744
|
-
const
|
|
1745
|
-
|
|
2117
|
+
const isNgDevModeGuarded = isNgDevModeConditional(description);
|
|
2118
|
+
const fixes = [
|
|
2119
|
+
fixer.replaceText(isNgDevModeGuarded ? fixedDescription : description, isNgDevModeGuarded
|
|
2120
|
+
? prependTokenName(sourceCode.getText(fixedDescription), name)
|
|
2121
|
+
: `${NG_DEV_MODE} ? ${prependTokenName(sourceCode.getText(fixedDescription), name)} : ''`),
|
|
2122
|
+
];
|
|
2123
|
+
if (!isNgDevModeGuarded &&
|
|
2124
|
+
shouldAddNgDevModeDeclaration &&
|
|
2125
|
+
!isNgDevModeVisible(sourceCode, description)) {
|
|
2126
|
+
shouldAddNgDevModeDeclaration = false;
|
|
2127
|
+
fixes.unshift(getNgDevModeDeclarationFix(program, fixer));
|
|
2128
|
+
}
|
|
2129
|
+
return fixes;
|
|
1746
2130
|
},
|
|
1747
2131
|
messageId: MESSAGE_ID$5,
|
|
1748
2132
|
node: description,
|
|
@@ -5315,52 +5699,6 @@ const rule$1 = createRule$2({
|
|
|
5315
5699
|
name: 'short-tui-imports',
|
|
5316
5700
|
});
|
|
5317
5701
|
|
|
5318
|
-
function isObject(node) {
|
|
5319
|
-
return node?.type === AST_NODE_TYPES$1.ObjectExpression;
|
|
5320
|
-
}
|
|
5321
|
-
|
|
5322
|
-
/**
|
|
5323
|
-
* Extracts the metadata object from a class decorator such as
|
|
5324
|
-
* `@Component()`, `@Directive()`, `@NgModule()`, or `@Pipe()`.
|
|
5325
|
-
*
|
|
5326
|
-
* Returns the first argument of the decorator call *if and only if*
|
|
5327
|
-
* it is an `ObjectExpression`.
|
|
5328
|
-
*
|
|
5329
|
-
* @example
|
|
5330
|
-
* // Given:
|
|
5331
|
-
* @Component({
|
|
5332
|
-
* selector: 'x',
|
|
5333
|
-
* imports: [A, B],
|
|
5334
|
-
* })
|
|
5335
|
-
* class MyCmp {}
|
|
5336
|
-
*
|
|
5337
|
-
* // In the AST for @Component(...)
|
|
5338
|
-
* getDecoratorMetadata(decorator, allowed) →
|
|
5339
|
-
* ObjectExpression({ selector: ..., imports: ... })
|
|
5340
|
-
*
|
|
5341
|
-
* @param decorator - The decorator node attached to a class declaration.
|
|
5342
|
-
* @param allowedNames - A set of decorator names to consider
|
|
5343
|
-
* (e.g., Component, Directive, NgModule, Pipe).
|
|
5344
|
-
*
|
|
5345
|
-
* @returns The metadata `ObjectExpression` if present and valid,
|
|
5346
|
-
* otherwise `null`.
|
|
5347
|
-
*/
|
|
5348
|
-
function getDecoratorMetadata(decorator, allowedNames) {
|
|
5349
|
-
const expr = decorator.expression;
|
|
5350
|
-
if (expr.type !== AST_NODE_TYPES$1.CallExpression) {
|
|
5351
|
-
return null;
|
|
5352
|
-
}
|
|
5353
|
-
const callee = expr.callee;
|
|
5354
|
-
if (callee.type !== AST_NODE_TYPES$1.Identifier) {
|
|
5355
|
-
return null;
|
|
5356
|
-
}
|
|
5357
|
-
if (!allowedNames.has(callee.name)) {
|
|
5358
|
-
return null;
|
|
5359
|
-
}
|
|
5360
|
-
const arg = expr.arguments[0];
|
|
5361
|
-
return isObject(arg) ? arg : null;
|
|
5362
|
-
}
|
|
5363
|
-
|
|
5364
5702
|
function getImportsArray(meta) {
|
|
5365
5703
|
const property = meta.properties.find((literal) => literal.type === AST_NODE_TYPES$1.Property &&
|
|
5366
5704
|
literal.key.type === AST_NODE_TYPES$1.Identifier &&
|
|
@@ -5436,10 +5774,6 @@ function getSortedNames(elements, source) {
|
|
|
5436
5774
|
return [...sortedRegular, ...sortedSpreads].map((n) => nameOf(n, source));
|
|
5437
5775
|
}
|
|
5438
5776
|
|
|
5439
|
-
function sameOrder(a, b) {
|
|
5440
|
-
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
5441
|
-
}
|
|
5442
|
-
|
|
5443
5777
|
const createRule$1 = ESLintUtils.RuleCreator((name) => name);
|
|
5444
5778
|
var standaloneImportsSort = createRule$1({
|
|
5445
5779
|
create(context, [options]) {
|
|
@@ -5609,6 +5943,7 @@ const plugin = {
|
|
|
5609
5943
|
'class-property-naming': classPropertyNaming,
|
|
5610
5944
|
'decorator-key-sort': config$3,
|
|
5611
5945
|
'flat-exports': flatExports,
|
|
5946
|
+
'host-attributes-sort': rule$i,
|
|
5612
5947
|
'html-logical-properties': config$2,
|
|
5613
5948
|
'injection-token-description': rule$h,
|
|
5614
5949
|
'no-deep-imports': rule$g,
|
package/package.json
CHANGED
|
@@ -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;
|