@ethlete/eslint-plugin 0.1.0-next.0 → 0.1.0-next.10
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/CHANGELOG.md +64 -0
- package/package.json +6 -5
- package/src/configs/recommended.js +414 -0
- package/src/index.js +138 -0
- package/src/rules/angular-decorator-property-order.js +205 -0
- package/src/rules/class-constant-property.js +365 -0
- package/src/rules/class-member-order.js +456 -0
- package/src/rules/enforce-routing-view-naming.js +84 -0
- package/src/rules/guard-return-newline.js +81 -0
- package/src/rules/inject-member-accessibility.js +93 -0
- package/src/rules/internals/angular-member-visibility.js +233 -0
- package/src/rules/internals/implemented-contract-members.js +294 -0
- package/src/rules/no-angular-router-api.js +158 -0
- package/src/rules/no-angular-seo-services.js +82 -0
- package/src/rules/no-direct-dom-manipulation.js +160 -0
- package/src/rules/no-document-cookie.js +55 -0
- package/src/rules/no-dom-query.js +81 -0
- package/src/rules/no-empty-angular-metadata-arrays.js +104 -0
- package/src/rules/no-empty-newlines-between-imports.js +45 -0
- package/src/rules/no-inject-chain.js +44 -0
- package/src/rules/no-leading-underscore-class-member.js +120 -0
- package/src/rules/no-legacy-angular-decorators.js +405 -0
- package/src/rules/no-locale-id.js +57 -0
- package/src/rules/no-member-alias.js +151 -0
- package/src/rules/no-native-observers.js +102 -0
- package/src/rules/no-pipe-logic.js +76 -0
- package/src/rules/no-public-property.js +85 -0
- package/src/rules/no-readonly-signal.js +119 -0
- package/src/rules/no-redundant-internal.js +94 -0
- package/src/rules/no-redundant-on-push-change-detection.js +237 -0
- package/src/rules/no-rxjs-in-effect.js +88 -0
- package/src/rules/no-screaming-case-local.js +113 -0
- package/src/rules/no-standalone-flag.js +95 -0
- package/src/rules/no-subscribe-in-pipe.js +89 -0
- package/src/rules/no-subscribe-with-body.js +95 -0
- package/src/rules/no-trivial-return-type.js +104 -0
- package/src/rules/no-trivial-wrapper-method.js +120 -0
- package/src/rules/no-type-only-import.js +79 -0
- package/src/rules/no-typed-injected-element-ref.js +88 -0
- package/src/rules/no-unused-class-member.js +209 -0
- package/src/rules/no-window-location.js +109 -0
- package/src/rules/prefer-clone-equal.js +112 -0
- package/src/rules/prefer-concise-angular-host-directives.js +225 -0
- package/src/rules/prefer-concise-angular-style-metadata.js +108 -0
- package/src/rules/prefer-element-dimensions.js +128 -0
- package/src/rules/prefer-linked-signal.js +124 -0
- package/src/rules/prefer-match-media.js +101 -0
- package/src/rules/prefer-rxjs-timer.js +82 -0
- package/src/rules/prefer-scroll-state.js +158 -0
- package/src/rules/prefer-viewport-size.js +75 -0
- package/src/rules/require-dollar-suffix.js +123 -0
- package/src/rules/require-on-push-change-detection.js +320 -0
- package/src/rules/require-view-encapsulation-none.js +264 -0
- package/src/rules/template-member-accessibility.js +204 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-next.10
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`4e9f2b4`](https://github.com/ethlete-io/ethdk/commit/4e9f2b4d12335fafef192350aef8ffc584996a91) Thanks [@TomTomB](https://github.com/TomTomB)! - Make the OnPush change detection rules Angular-version aware. Since OnPush is the default from Angular 22, declaring it is now redundant:
|
|
8
|
+
- Add `no-redundant-on-push-change-detection`, which flags and auto-fixes explicit `changeDetection: ChangeDetectionStrategy.OnPush` on `@Component`, and removes the now-unused `ChangeDetectionStrategy` import when it is no longer referenced. Active on Angular >= 22.
|
|
9
|
+
- `require-on-push-change-detection` now only applies on Angular <= 21, where OnPush is opt-in.
|
|
10
|
+
|
|
11
|
+
Both rules resolve the workspace's Angular major version automatically (overridable via `settings.ethlete.angularMajor`) and stay inert when they don't apply, so exactly one of them is ever active for a given Angular version.
|
|
12
|
+
|
|
13
|
+
## 0.1.0-next.9
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`b323ef6`](https://github.com/ethlete-io/ethdk/commit/b323ef66130d196e5c893e844d50ecfc85487373) Thanks [@TomTomB](https://github.com/TomTomB)! - `class-member-order`: recognize custom `injectXyz()` helper functions (not just the raw `inject()` call) as inject-group members, so they're required to be declared before inputs/outputs/queries/properties like other injected dependencies.
|
|
18
|
+
|
|
19
|
+
## 0.1.0-next.8
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- [`edea44b`](https://github.com/ethlete-io/ethdk/commit/edea44bf1c494420f02b545202f4b24db9a6395c) Thanks [@TomTomB](https://github.com/TomTomB)! - Update to angular 22
|
|
24
|
+
|
|
25
|
+
## 0.1.0-next.7
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- [`7e8ac6b`](https://github.com/ethlete-io/ethdk/commit/7e8ac6bf18f580289c7c7966c6c33a2a7f077299) Thanks [@TomTomB](https://github.com/TomTomB)! - Fix false positives in pipes
|
|
30
|
+
|
|
31
|
+
## 0.1.0-next.6
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- [`b73a127`](https://github.com/ethlete-io/ethdk/commit/b73a127002a06e3aa0c4e7e977b1ad1f3e04e7e6) Thanks [@TomTomB](https://github.com/TomTomB)! - Bump yet again, final one for sure, pinky promise
|
|
36
|
+
|
|
37
|
+
## 0.1.0-beta.5
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- [`ddb5d09`](https://github.com/ethlete-io/ethdk/commit/ddb5d09e4bc56e18cc8c228aa78a200441e7a766) Thanks [@TomTomB](https://github.com/TomTomB)! - Bump to beta
|
|
42
|
+
|
|
43
|
+
## 0.1.0-next.4
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- [`a690217`](https://github.com/ethlete-io/ethdk/commit/a6902172efd9bd1956a16237e79acbfbd816d946) Thanks [@TomTomB](https://github.com/TomTomB)! - Version bump only
|
|
48
|
+
|
|
49
|
+
## 0.1.0-next.3
|
|
50
|
+
|
|
51
|
+
### Patch Changes
|
|
52
|
+
|
|
53
|
+
- [`5d1d3ac`](https://github.com/ethlete-io/ethdk/commit/5d1d3accbd4a7657bc50cdc0653a7ca24fe761e2) Thanks [@TomTomB](https://github.com/TomTomB)! - Fix minor lint edge cases
|
|
54
|
+
|
|
55
|
+
## 0.1.0-next.2
|
|
56
|
+
|
|
57
|
+
### Patch Changes
|
|
58
|
+
|
|
59
|
+
- [#2957](https://github.com/ethlete-io/ethdk/pull/2957) [`81173d4`](https://github.com/ethlete-io/ethdk/commit/81173d426d1a53ea2a7af0d16d6eb0fe4a16dbd9) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add missing auto fixers
|
|
60
|
+
|
|
61
|
+
## 0.1.0-next.1
|
|
62
|
+
|
|
63
|
+
### Patch Changes
|
|
64
|
+
|
|
65
|
+
- [`6ec51b4`](https://github.com/ethlete-io/ethdk/commit/6ec51b49497ba53bf34ec0aabcf519957ba0e412) Thanks [@TomTomB](https://github.com/TomTomB)! - Fix plugin not including js files
|
|
66
|
+
|
|
3
67
|
## 0.1.0-next.0
|
|
4
68
|
|
|
5
69
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/eslint-plugin",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.10",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"peerDependencies": {
|
|
8
8
|
"@angular-eslint/eslint-plugin-template": ">=21.0.0",
|
|
9
|
+
"@angular/core": "22.0.5",
|
|
9
10
|
"@typescript-eslint/eslint-plugin": ">=8.0.0",
|
|
10
11
|
"eslint": ">=9.0.0",
|
|
11
|
-
"tslib": "2.8.1"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
12
|
+
"tslib": "2.8.1",
|
|
13
|
+
"typescript": "6.0.3"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flat config entries for TypeScript files.
|
|
6
|
+
* Covers all lintable rules from the Ethlete styleguide.
|
|
7
|
+
*
|
|
8
|
+
* @type {import('eslint').Linter.Config}
|
|
9
|
+
*/
|
|
10
|
+
const recommendedTs = {
|
|
11
|
+
files: ['**/*.ts'],
|
|
12
|
+
plugins: {
|
|
13
|
+
// The plugin itself is injected by the caller (see index.js)
|
|
14
|
+
},
|
|
15
|
+
rules: {
|
|
16
|
+
// ── TypeScript ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
// No interface — use type
|
|
19
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
20
|
+
|
|
21
|
+
// No any
|
|
22
|
+
'@typescript-eslint/no-explicit-any': 'error',
|
|
23
|
+
|
|
24
|
+
// No declared-but-never-read variables, parameters, or imports
|
|
25
|
+
// Args prefixed with _ are exempt (intentionally unused callback params)
|
|
26
|
+
'no-unused-vars': 'off', // disabled in favour of the TS-aware version below
|
|
27
|
+
'@typescript-eslint/no-unused-vars': [
|
|
28
|
+
'error',
|
|
29
|
+
{
|
|
30
|
+
vars: 'all',
|
|
31
|
+
args: 'all',
|
|
32
|
+
argsIgnorePattern: '^_',
|
|
33
|
+
caughtErrors: 'all',
|
|
34
|
+
caughtErrorsIgnorePattern: '^_',
|
|
35
|
+
ignoreRestSiblings: true,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
// ── Naming & formatting ─────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
// No var
|
|
42
|
+
'no-var': 'error',
|
|
43
|
+
|
|
44
|
+
// No unnecessary let — default to const
|
|
45
|
+
'prefer-const': 'error',
|
|
46
|
+
|
|
47
|
+
// Never declare multiple variables in one statement
|
|
48
|
+
'one-var': ['error', 'never'],
|
|
49
|
+
|
|
50
|
+
// Always === / !==
|
|
51
|
+
eqeqeq: ['error', 'always'],
|
|
52
|
+
|
|
53
|
+
// Max two function parameters
|
|
54
|
+
'max-params': ['error', 2],
|
|
55
|
+
|
|
56
|
+
// No _ or # prefixes; enforce camelCase / PascalCase / UPPER_CASE
|
|
57
|
+
'@typescript-eslint/naming-convention': [
|
|
58
|
+
'error',
|
|
59
|
+
{
|
|
60
|
+
selector: 'default',
|
|
61
|
+
leadingUnderscore: 'forbid',
|
|
62
|
+
trailingUnderscore: 'forbid',
|
|
63
|
+
format: null,
|
|
64
|
+
},
|
|
65
|
+
// Allow any format for object literal properties that require quotes
|
|
66
|
+
// (e.g. Angular host binding keys: '[class.foo]', '(click)', '[attr.aria-label]')
|
|
67
|
+
{
|
|
68
|
+
selector: 'objectLiteralProperty',
|
|
69
|
+
modifiers: ['requiresQuotes'],
|
|
70
|
+
format: null,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
selector: ['variable', 'parameter', 'property', 'parameterProperty', 'accessor'],
|
|
74
|
+
leadingUnderscore: 'allow',
|
|
75
|
+
trailingUnderscore: 'forbid',
|
|
76
|
+
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
selector: 'method',
|
|
80
|
+
leadingUnderscore: 'allow',
|
|
81
|
+
trailingUnderscore: 'forbid',
|
|
82
|
+
format: null,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
selector: 'typeLike',
|
|
86
|
+
leadingUnderscore: 'forbid',
|
|
87
|
+
trailingUnderscore: 'forbid',
|
|
88
|
+
format: ['PascalCase'],
|
|
89
|
+
},
|
|
90
|
+
// Generic type parameters must be T-prefixed: TValue, TResult — never bare T
|
|
91
|
+
{
|
|
92
|
+
selector: 'typeParameter',
|
|
93
|
+
leadingUnderscore: 'forbid',
|
|
94
|
+
trailingUnderscore: 'forbid',
|
|
95
|
+
prefix: ['T'],
|
|
96
|
+
format: ['PascalCase'],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
selector: 'enumMember',
|
|
100
|
+
leadingUnderscore: 'forbid',
|
|
101
|
+
trailingUnderscore: 'forbid',
|
|
102
|
+
format: ['UPPER_CASE', 'PascalCase'],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
|
|
106
|
+
// ── Banned syntax ───────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
'no-restricted-syntax': [
|
|
109
|
+
'error',
|
|
110
|
+
// No enum — use const object + derived union type
|
|
111
|
+
{
|
|
112
|
+
selector: 'TSEnumDeclaration',
|
|
113
|
+
message: 'No enums. Use a const object + derived union type instead.',
|
|
114
|
+
},
|
|
115
|
+
// No standalone function declarations / expressions — use arrow functions
|
|
116
|
+
{
|
|
117
|
+
selector: 'FunctionDeclaration',
|
|
118
|
+
message: 'No function declarations. Use arrow functions instead.',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
selector: "FunctionExpression:not([parent.type='Property']):not([parent.type='MethodDefinition'])",
|
|
122
|
+
message: 'No function expressions outside of object methods or class methods. Use arrow functions.',
|
|
123
|
+
},
|
|
124
|
+
// No arrow properties in classes — use regular methods
|
|
125
|
+
{
|
|
126
|
+
selector: 'PropertyDefinition > ArrowFunctionExpression',
|
|
127
|
+
message: 'No arrow function properties in classes. Use regular methods.',
|
|
128
|
+
},
|
|
129
|
+
// No async/await — use RxJS
|
|
130
|
+
{
|
|
131
|
+
selector: 'FunctionDeclaration[async=true]',
|
|
132
|
+
message: 'No async/await. Use RxJS for all async operations.',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
selector: 'ArrowFunctionExpression[async=true]',
|
|
136
|
+
message: 'No async/await. Use RxJS for all async operations.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
selector: 'FunctionExpression[async=true]',
|
|
140
|
+
message: 'No async/await. Use RxJS for all async operations.',
|
|
141
|
+
},
|
|
142
|
+
// No static class members
|
|
143
|
+
{
|
|
144
|
+
selector: 'MethodDefinition[static=true]',
|
|
145
|
+
message: 'No static class members.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
selector: 'PropertyDefinition[static=true]',
|
|
149
|
+
message: 'No static class members.',
|
|
150
|
+
},
|
|
151
|
+
// No # prefix — use private keyword
|
|
152
|
+
{
|
|
153
|
+
selector: "PropertyDefinition[key.type='PrivateIdentifier']",
|
|
154
|
+
message: 'No # prefix on class members. Use the private keyword instead.',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
selector: "MethodDefinition[key.type='PrivateIdentifier']",
|
|
158
|
+
message: 'No # prefix on class members. Use the private keyword instead.',
|
|
159
|
+
},
|
|
160
|
+
// No constructor injection — use inject()
|
|
161
|
+
{
|
|
162
|
+
selector: 'TSParameterProperty[accessibility]',
|
|
163
|
+
message: 'No constructor injection. Use inject() instead.',
|
|
164
|
+
},
|
|
165
|
+
// No legacy Angular lifecycle hooks
|
|
166
|
+
{
|
|
167
|
+
selector:
|
|
168
|
+
'MethodDefinition[key.name=/^ngOnChanges$|^ngAfterViewInit$|^ngAfterContentInit$|^ngAfterContentChecked$|^ngAfterViewChecked$|^ngDoCheck$/]',
|
|
169
|
+
message:
|
|
170
|
+
'No legacy Angular lifecycle hooks. Use afterNextRender, inject(DestroyRef).onDestroy, or constructor-based effects instead.',
|
|
171
|
+
},
|
|
172
|
+
// No @Injectable — use createRootProvider / createProvider from @ethlete/core
|
|
173
|
+
{
|
|
174
|
+
selector: "Decorator[expression.callee.name='Injectable'], Decorator[expression.name='Injectable']",
|
|
175
|
+
message: 'No @Injectable services. Use createRootProvider / createProvider from @ethlete/core instead.',
|
|
176
|
+
},
|
|
177
|
+
// No route guards — handle access control inside the component itself
|
|
178
|
+
{
|
|
179
|
+
selector:
|
|
180
|
+
'TSClassImplements > Identifier.expression[name=/^CanActivate$|^CanDeactivate$|^CanActivateChild$|^CanMatch$|^CanLoad$/]',
|
|
181
|
+
message: 'No route guards. Handle access control inside the component itself.',
|
|
182
|
+
},
|
|
183
|
+
// No route resolvers — use the query library to fetch data
|
|
184
|
+
{
|
|
185
|
+
selector: "TSClassImplements > Identifier.expression[name='Resolve']",
|
|
186
|
+
message: 'No route resolvers. Use the query library to fetch data instead.',
|
|
187
|
+
},
|
|
188
|
+
// No barrel imports — import directly from the source file
|
|
189
|
+
{
|
|
190
|
+
selector: 'ImportDeclaration[source.value=/(^|[/])index$/]',
|
|
191
|
+
message:
|
|
192
|
+
'No barrel imports (index files). Import directly from the source file to avoid breaking lazy loading.',
|
|
193
|
+
},
|
|
194
|
+
// No on-prefixed method names — name the method after what it does, not that it handles an event
|
|
195
|
+
{
|
|
196
|
+
selector: 'MethodDefinition[key.name=/^on[A-Z]/]',
|
|
197
|
+
message:
|
|
198
|
+
'No on-prefixed method names. Name the method after what it does (e.g. onLayoutChange -> syncGridItemsWithLayout).',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
|
|
202
|
+
// ── Native DOM globals ──────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
// No direct document/window access — use inject(DOCUMENT) / inject(WINDOW) instead
|
|
205
|
+
'no-restricted-globals': [
|
|
206
|
+
'error',
|
|
207
|
+
{
|
|
208
|
+
name: 'document',
|
|
209
|
+
message: "Use inject(DOCUMENT) from '@angular/common' instead of the global document.",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: 'window',
|
|
213
|
+
message: 'Avoid accessing the global window directly. Use platform detection or a dedicated injection token.',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
|
|
217
|
+
// ── Custom plugin rules ─────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
// No inject(X).member chaining — assign to a const first
|
|
220
|
+
'ethlete/no-inject-chain': 'error',
|
|
221
|
+
|
|
222
|
+
// inject(ElementRef) must use inject<ElementRef<HTMLElement>>(ElementRef) — not inject(ElementRef) or inject(ElementRef<...>)
|
|
223
|
+
'ethlete/no-typed-injected-element-ref': 'error',
|
|
224
|
+
|
|
225
|
+
// No @internal on members already hidden by private/protected
|
|
226
|
+
'ethlete/no-redundant-internal': 'error',
|
|
227
|
+
|
|
228
|
+
// Non-injected Angular members should use explicit accessibility when they
|
|
229
|
+
// participate in the reachable surface. Template/host members must be
|
|
230
|
+
// explicit, other implicit-public members must be public. @internal does
|
|
231
|
+
// not replace explicit public; protected remains valid for template-only
|
|
232
|
+
// members.
|
|
233
|
+
'ethlete/template-member-accessibility': 'error',
|
|
234
|
+
|
|
235
|
+
// No explicit trivial return types TypeScript can infer
|
|
236
|
+
'ethlete/no-trivial-return-type': 'error',
|
|
237
|
+
|
|
238
|
+
// Empty line required before return in multi-statement if-blocks (guard clauses)
|
|
239
|
+
'ethlete/guard-return-newline': 'error',
|
|
240
|
+
|
|
241
|
+
// No code in subscribe() callbacks — use tap() instead
|
|
242
|
+
'ethlete/no-subscribe-with-body': 'error',
|
|
243
|
+
|
|
244
|
+
// Observable variables/properties must end with $
|
|
245
|
+
'ethlete/require-dollar-suffix': 'error',
|
|
246
|
+
|
|
247
|
+
// No .subscribe() inside a .pipe() callback
|
|
248
|
+
'ethlete/no-subscribe-in-pipe': 'error',
|
|
249
|
+
|
|
250
|
+
// No .subscribe() inside effect() or computed()
|
|
251
|
+
'ethlete/no-rxjs-in-effect': 'error',
|
|
252
|
+
|
|
253
|
+
// No readonly on reactive class properties (signals, inputs, computed, inject, etc.)
|
|
254
|
+
'ethlete/no-readonly-signal': 'error',
|
|
255
|
+
|
|
256
|
+
// No SCREAMING_CASE variable names inside function bodies
|
|
257
|
+
'ethlete/no-screaming-case-local': 'error',
|
|
258
|
+
|
|
259
|
+
// Prefer RxJS timer/interval/fromEvent over setTimeout/setInterval/addEventListener
|
|
260
|
+
'ethlete/prefer-rxjs-timer': 'error',
|
|
261
|
+
|
|
262
|
+
// Prefer linkedSignal over signal + .set() inside effect()
|
|
263
|
+
'ethlete/prefer-linked-signal': 'warn',
|
|
264
|
+
|
|
265
|
+
// No trivial wrapper methods that only forward all args to another call
|
|
266
|
+
'ethlete/no-trivial-wrapper-method': 'error',
|
|
267
|
+
|
|
268
|
+
// No direct DOM manipulation — use injectRenderer() from @ethlete/core instead
|
|
269
|
+
'ethlete/no-direct-dom-manipulation': 'error',
|
|
270
|
+
|
|
271
|
+
// No raw IntersectionObserver / MutationObserver / ResizeObserver — use @ethlete/core signal utils
|
|
272
|
+
'ethlete/no-native-observers': 'error',
|
|
273
|
+
|
|
274
|
+
// Prefer injectViewportSize() over window.innerWidth / innerHeight / outerWidth / outerHeight
|
|
275
|
+
'ethlete/prefer-viewport-size': 'warn',
|
|
276
|
+
|
|
277
|
+
// Prefer injectMediaQueryIsMatched() / injectBreakpointIsMatched() etc. over .matchMedia()
|
|
278
|
+
'ethlete/prefer-match-media': 'error',
|
|
279
|
+
|
|
280
|
+
// Prefer signalElementDimensions() / signalHostElementDimensions() in reactive contexts
|
|
281
|
+
'ethlete/prefer-element-dimensions': 'warn',
|
|
282
|
+
|
|
283
|
+
// Prefer signalElementScrollState() / signalHostElementScrollState() in reactive contexts
|
|
284
|
+
'ethlete/prefer-scroll-state': 'warn',
|
|
285
|
+
|
|
286
|
+
// No `import type { Foo }` or `import { type Foo }` — use regular value imports
|
|
287
|
+
'ethlete/no-type-only-import': 'error',
|
|
288
|
+
|
|
289
|
+
// No empty blank lines between import declarations
|
|
290
|
+
'ethlete/no-empty-newlines-between-imports': 'error',
|
|
291
|
+
|
|
292
|
+
// No legacy Angular decorators — use signal-based APIs (input, output, viewChild, etc.)
|
|
293
|
+
// and host: {} bindings (instead of @HostBinding / @HostListener)
|
|
294
|
+
'ethlete/no-legacy-angular-decorators': 'error',
|
|
295
|
+
|
|
296
|
+
// No Angular Title/Meta services — use @ethlete/core SEO utilities instead
|
|
297
|
+
// (injectTitleBinding, injectMetaBinding, injectLinkBinding, etc.)
|
|
298
|
+
'ethlete/no-angular-seo-services': 'error',
|
|
299
|
+
|
|
300
|
+
// No JSON.parse(JSON.stringify()), structuredClone, or lodash cloneDeep / isEqual
|
|
301
|
+
// — use clone() / equal() from '@ethlete/core' instead
|
|
302
|
+
'ethlete/prefer-clone-equal': 'error',
|
|
303
|
+
|
|
304
|
+
// No direct document.cookie access
|
|
305
|
+
// — use getCookie / setCookie / hasCookie / deleteCookie from '@ethlete/core' instead
|
|
306
|
+
'ethlete/no-document-cookie': 'error',
|
|
307
|
+
|
|
308
|
+
// Injected providers must be private by default, and protected only when
|
|
309
|
+
// referenced from an Angular template or host binding.
|
|
310
|
+
'ethlete/inject-member-accessibility': 'error',
|
|
311
|
+
|
|
312
|
+
// No ActivatedRoute (fully replaced by inject* router utilities from @ethlete/core)
|
|
313
|
+
// No inject(Router) for state reading — use injectUrl/injectRoute/injectQueryParam etc.
|
|
314
|
+
'ethlete/no-angular-router-api': 'warn',
|
|
315
|
+
|
|
316
|
+
// No window.location.* reads for URL state — use injectUrl/injectRoute/injectQueryParam etc.
|
|
317
|
+
// No new URLSearchParams(window.location.search) — use injectQueryParams()
|
|
318
|
+
'ethlete/no-window-location': 'warn',
|
|
319
|
+
|
|
320
|
+
// No inject(LOCALE_ID) — use injectLocale() from '@ethlete/core'
|
|
321
|
+
'ethlete/no-locale-id': 'error',
|
|
322
|
+
|
|
323
|
+
// No class members that are pure aliases for a nested property of another member
|
|
324
|
+
// — widen the accessibility of the source member instead
|
|
325
|
+
'ethlete/no-member-alias': 'error',
|
|
326
|
+
|
|
327
|
+
// No leading underscores on class members; auto-fix private members when safe
|
|
328
|
+
'ethlete/no-leading-underscore-class-member': 'error',
|
|
329
|
+
|
|
330
|
+
// No declared-but-never-read private/protected class members
|
|
331
|
+
'ethlete/no-unused-class-member': 'error',
|
|
332
|
+
|
|
333
|
+
// No direct DOM query methods (querySelector, getElementById, etc.)
|
|
334
|
+
// — use viewChild() / viewChildren() or contentChild() / contentChildren() instead
|
|
335
|
+
'ethlete/no-dom-query': 'error',
|
|
336
|
+
|
|
337
|
+
// Remove empty imports: [] and hostDirectives: [] metadata noise
|
|
338
|
+
'ethlete/no-empty-angular-metadata-arrays': 'error',
|
|
339
|
+
|
|
340
|
+
// Remove standalone metadata — standalone is implicit and should not be declared
|
|
341
|
+
'ethlete/no-standalone-flag': 'error',
|
|
342
|
+
|
|
343
|
+
// ── Angular outputs ─────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
// No on-prefixed outputs (onSelectDate → selectDate)
|
|
346
|
+
'@angular-eslint/no-output-on-prefix': 'error',
|
|
347
|
+
|
|
348
|
+
// No outputs named after native DOM events (change, click, etc.)
|
|
349
|
+
'@angular-eslint/no-output-native': 'error',
|
|
350
|
+
|
|
351
|
+
// ── Angular components ──────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
// Require ChangeDetectionStrategy.OnPush on Angular <= 21, where it is opt-in
|
|
354
|
+
// (version-aware: inert on Angular 22+, where OnPush is the default).
|
|
355
|
+
'ethlete/require-on-push-change-detection': 'error',
|
|
356
|
+
|
|
357
|
+
// No redundant ChangeDetectionStrategy.OnPush — it is the default since Angular 22
|
|
358
|
+
// (version-aware: inert on older Angular). Also removes the now-unused import.
|
|
359
|
+
'ethlete/no-redundant-on-push-change-detection': 'error',
|
|
360
|
+
|
|
361
|
+
// Always use ViewEncapsulation.None (angular-eslint's use-component-view-encapsulation does the
|
|
362
|
+
// opposite — it disallows None, so we use our own custom rule instead)
|
|
363
|
+
'ethlete/require-view-encapsulation-none': 'error',
|
|
364
|
+
|
|
365
|
+
// Keep Angular @Component / @Directive metadata in a consistent order
|
|
366
|
+
'ethlete/angular-decorator-property-order': 'error',
|
|
367
|
+
|
|
368
|
+
// Keep class members in the documented styleguide order
|
|
369
|
+
'ethlete/class-member-order': 'error',
|
|
370
|
+
|
|
371
|
+
// True class constants should be readonly and use SCREAMING_CASE
|
|
372
|
+
'ethlete/class-constant-property': 'error',
|
|
373
|
+
|
|
374
|
+
// Prefer shorthand hostDirectives entries and keep extended host directive configs ordered
|
|
375
|
+
'ethlete/prefer-concise-angular-host-directives': 'error',
|
|
376
|
+
|
|
377
|
+
// Prefer concise Angular style metadata when there is only one entry
|
|
378
|
+
'ethlete/prefer-concise-angular-style-metadata': 'error',
|
|
379
|
+
|
|
380
|
+
// No logic in pipe transform — assign to an external utility function instead
|
|
381
|
+
'ethlete/no-pipe-logic': 'error',
|
|
382
|
+
|
|
383
|
+
// Routing components must use paths containing "-view" and class names ending in "ViewComponent"
|
|
384
|
+
'ethlete/enforce-routing-view-naming': 'error',
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Flat config entries for Angular HTML template files.
|
|
390
|
+
* @type {import('eslint').Linter.Config}
|
|
391
|
+
*/
|
|
392
|
+
const recommendedTemplate = {
|
|
393
|
+
files: ['**/*.html'],
|
|
394
|
+
rules: {
|
|
395
|
+
// No $any() in templates
|
|
396
|
+
'@angular-eslint/template/no-any': 'error',
|
|
397
|
+
// Prefer plain attribute over unnecessary property binding for static strings: etIcon="foo" not [etIcon]="'foo'"
|
|
398
|
+
'@angular-eslint/template/prefer-static-string-properties': 'error',
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Relaxed rules for test/spec files.
|
|
404
|
+
* Non-null assertions are common and intentional in tests.
|
|
405
|
+
* @type {import('eslint').Linter.Config}
|
|
406
|
+
*/
|
|
407
|
+
const recommendedSpec = {
|
|
408
|
+
files: ['**/*.spec.ts'],
|
|
409
|
+
rules: {
|
|
410
|
+
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
module.exports = { recommendedTs, recommendedTemplate, recommendedSpec };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const noInjectChain = require('./rules/no-inject-chain');
|
|
5
|
+
const noTrivialReturnType = require('./rules/no-trivial-return-type');
|
|
6
|
+
const noSubscribeInPipe = require('./rules/no-subscribe-in-pipe');
|
|
7
|
+
const noRxjsInEffect = require('./rules/no-rxjs-in-effect');
|
|
8
|
+
const noReadonlySignal = require('./rules/no-readonly-signal');
|
|
9
|
+
const noScreamingCaseLocal = require('./rules/no-screaming-case-local');
|
|
10
|
+
const preferRxjsTimer = require('./rules/prefer-rxjs-timer');
|
|
11
|
+
const preferLinkedSignal = require('./rules/prefer-linked-signal');
|
|
12
|
+
const noTrivialWrapperMethod = require('./rules/no-trivial-wrapper-method');
|
|
13
|
+
const noDirectDomManipulation = require('./rules/no-direct-dom-manipulation');
|
|
14
|
+
const noNativeObservers = require('./rules/no-native-observers');
|
|
15
|
+
const preferViewportSize = require('./rules/prefer-viewport-size');
|
|
16
|
+
const preferMatchMedia = require('./rules/prefer-match-media');
|
|
17
|
+
const preferElementDimensions = require('./rules/prefer-element-dimensions');
|
|
18
|
+
const preferScrollState = require('./rules/prefer-scroll-state');
|
|
19
|
+
const noTypeOnlyImport = require('./rules/no-type-only-import');
|
|
20
|
+
const guardReturnNewline = require('./rules/guard-return-newline');
|
|
21
|
+
const noSubscribeWithBody = require('./rules/no-subscribe-with-body');
|
|
22
|
+
const requireDollarSuffix = require('./rules/require-dollar-suffix');
|
|
23
|
+
const noPipeLogic = require('./rules/no-pipe-logic');
|
|
24
|
+
const enforceRoutingViewNaming = require('./rules/enforce-routing-view-naming');
|
|
25
|
+
const noRedundantOnPushChangeDetection = require('./rules/no-redundant-on-push-change-detection');
|
|
26
|
+
const requireOnPushChangeDetection = require('./rules/require-on-push-change-detection');
|
|
27
|
+
const requireViewEncapsulationNone = require('./rules/require-view-encapsulation-none');
|
|
28
|
+
const noLegacyAngularDecorators = require('./rules/no-legacy-angular-decorators');
|
|
29
|
+
const noAngularSeoServices = require('./rules/no-angular-seo-services');
|
|
30
|
+
const preferCloneEqual = require('./rules/prefer-clone-equal');
|
|
31
|
+
const noDocumentCookie = require('./rules/no-document-cookie');
|
|
32
|
+
const injectMemberAccessibility = require('./rules/inject-member-accessibility');
|
|
33
|
+
const noPublicProperty = require('./rules/no-public-property');
|
|
34
|
+
const noRedundantInternal = require('./rules/no-redundant-internal');
|
|
35
|
+
const templateMemberAccessibility = require('./rules/template-member-accessibility');
|
|
36
|
+
const noAngularRouterApi = require('./rules/no-angular-router-api');
|
|
37
|
+
const noWindowLocation = require('./rules/no-window-location');
|
|
38
|
+
const noLocaleId = require('./rules/no-locale-id');
|
|
39
|
+
const noMemberAlias = require('./rules/no-member-alias');
|
|
40
|
+
const noUnusedClassMember = require('./rules/no-unused-class-member');
|
|
41
|
+
const noDomQuery = require('./rules/no-dom-query');
|
|
42
|
+
const noLeadingUnderscoreClassMember = require('./rules/no-leading-underscore-class-member');
|
|
43
|
+
const noEmptyAngularMetadataArrays = require('./rules/no-empty-angular-metadata-arrays');
|
|
44
|
+
const noStandaloneFlag = require('./rules/no-standalone-flag');
|
|
45
|
+
const noEmptyNewlinesBetweenImports = require('./rules/no-empty-newlines-between-imports');
|
|
46
|
+
const angularDecoratorPropertyOrder = require('./rules/angular-decorator-property-order');
|
|
47
|
+
const classMemberOrder = require('./rules/class-member-order');
|
|
48
|
+
const classConstantProperty = require('./rules/class-constant-property');
|
|
49
|
+
const preferConciseAngularHostDirectives = require('./rules/prefer-concise-angular-host-directives');
|
|
50
|
+
const preferConciseAngularStyleMetadata = require('./rules/prefer-concise-angular-style-metadata');
|
|
51
|
+
const noTypedInjectedElementRef = require('./rules/no-typed-injected-element-ref');
|
|
52
|
+
const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
|
|
53
|
+
|
|
54
|
+
/** @type {import('eslint').ESLint.Plugin} */
|
|
55
|
+
const plugin = {
|
|
56
|
+
meta: {
|
|
57
|
+
name: '@ethlete/eslint-plugin',
|
|
58
|
+
version: '0.0.1',
|
|
59
|
+
},
|
|
60
|
+
rules: {
|
|
61
|
+
'no-inject-chain': noInjectChain,
|
|
62
|
+
'no-trivial-return-type': noTrivialReturnType,
|
|
63
|
+
'no-subscribe-in-pipe': noSubscribeInPipe,
|
|
64
|
+
'no-rxjs-in-effect': noRxjsInEffect,
|
|
65
|
+
'no-readonly-signal': noReadonlySignal,
|
|
66
|
+
'no-screaming-case-local': noScreamingCaseLocal,
|
|
67
|
+
'prefer-rxjs-timer': preferRxjsTimer,
|
|
68
|
+
'prefer-linked-signal': preferLinkedSignal,
|
|
69
|
+
'no-trivial-wrapper-method': noTrivialWrapperMethod,
|
|
70
|
+
'no-direct-dom-manipulation': noDirectDomManipulation,
|
|
71
|
+
'no-native-observers': noNativeObservers,
|
|
72
|
+
'prefer-viewport-size': preferViewportSize,
|
|
73
|
+
'prefer-match-media': preferMatchMedia,
|
|
74
|
+
'prefer-element-dimensions': preferElementDimensions,
|
|
75
|
+
'prefer-scroll-state': preferScrollState,
|
|
76
|
+
'no-type-only-import': noTypeOnlyImport,
|
|
77
|
+
'guard-return-newline': guardReturnNewline,
|
|
78
|
+
'no-subscribe-with-body': noSubscribeWithBody,
|
|
79
|
+
'require-dollar-suffix': requireDollarSuffix,
|
|
80
|
+
'no-pipe-logic': noPipeLogic,
|
|
81
|
+
'enforce-routing-view-naming': enforceRoutingViewNaming,
|
|
82
|
+
'no-redundant-on-push-change-detection': noRedundantOnPushChangeDetection,
|
|
83
|
+
'require-on-push-change-detection': requireOnPushChangeDetection,
|
|
84
|
+
'require-view-encapsulation-none': requireViewEncapsulationNone,
|
|
85
|
+
'no-legacy-angular-decorators': noLegacyAngularDecorators,
|
|
86
|
+
'no-angular-seo-services': noAngularSeoServices,
|
|
87
|
+
'prefer-clone-equal': preferCloneEqual,
|
|
88
|
+
'no-document-cookie': noDocumentCookie,
|
|
89
|
+
'inject-member-accessibility': injectMemberAccessibility,
|
|
90
|
+
'no-public-property': noPublicProperty,
|
|
91
|
+
'no-redundant-internal': noRedundantInternal,
|
|
92
|
+
'template-member-accessibility': templateMemberAccessibility,
|
|
93
|
+
'no-angular-router-api': noAngularRouterApi,
|
|
94
|
+
'no-window-location': noWindowLocation,
|
|
95
|
+
'no-locale-id': noLocaleId,
|
|
96
|
+
'no-member-alias': noMemberAlias,
|
|
97
|
+
'no-unused-class-member': noUnusedClassMember,
|
|
98
|
+
'no-dom-query': noDomQuery,
|
|
99
|
+
'no-leading-underscore-class-member': noLeadingUnderscoreClassMember,
|
|
100
|
+
'no-empty-angular-metadata-arrays': noEmptyAngularMetadataArrays,
|
|
101
|
+
'no-standalone-flag': noStandaloneFlag,
|
|
102
|
+
'no-empty-newlines-between-imports': noEmptyNewlinesBetweenImports,
|
|
103
|
+
'angular-decorator-property-order': angularDecoratorPropertyOrder,
|
|
104
|
+
'class-member-order': classMemberOrder,
|
|
105
|
+
'class-constant-property': classConstantProperty,
|
|
106
|
+
'prefer-concise-angular-host-directives': preferConciseAngularHostDirectives,
|
|
107
|
+
'prefer-concise-angular-style-metadata': preferConciseAngularStyleMetadata,
|
|
108
|
+
'no-typed-injected-element-ref': noTypedInjectedElementRef,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Inject the plugin reference into the configs so consumers don't have to
|
|
113
|
+
/** @type {import('eslint').Linter.Config} */
|
|
114
|
+
const recommendedTsWithPlugin = {
|
|
115
|
+
...recommendedTs,
|
|
116
|
+
plugins: { ethlete: plugin },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** @type {import('eslint').Linter.Config} */
|
|
120
|
+
const recommendedTemplateWithPlugin = {
|
|
121
|
+
...recommendedTemplate,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const configs = {
|
|
125
|
+
/** TypeScript rules (merged into a files: ['**\/*.ts'] block) */
|
|
126
|
+
recommendedTs: recommendedTsWithPlugin,
|
|
127
|
+
/** Angular template rules (merged into a files: ['**\/*.html'] block) */
|
|
128
|
+
recommendedTemplate: recommendedTemplateWithPlugin,
|
|
129
|
+
/**
|
|
130
|
+
* Both together as a flat array — the most common usage:
|
|
131
|
+
* export default [...baseConfig, ...ethlete.configs.recommended]
|
|
132
|
+
*/
|
|
133
|
+
recommended: [recommendedTsWithPlugin, recommendedTemplateWithPlugin, recommendedSpec],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const ethletePlugin = { ...plugin, configs };
|
|
137
|
+
|
|
138
|
+
module.exports = ethletePlugin;
|