@ethlete/eslint-plugin 1.0.0-next.23 → 1.0.0-next.25

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/migrations.json +12 -0
  3. package/package.json +13 -3
  4. package/src/configs/recommended.js +33 -11
  5. package/src/generators/generators.json +10 -0
  6. package/src/generators/restore-module-augmentation-interfaces/migration.js +50 -0
  7. package/src/generators/restore-module-augmentation-interfaces/restore-module-augmentation-interfaces.js +103 -0
  8. package/src/generators/restore-module-augmentation-interfaces/schema.json +14 -0
  9. package/src/index.js +10 -2
  10. package/src/rules/angular-decorator-property-order.js +20 -20
  11. package/src/rules/class-constant-property.js +8 -14
  12. package/src/rules/class-member-order.js +77 -46
  13. package/src/rules/consistent-type-definitions.js +60 -0
  14. package/src/rules/inject-member-accessibility.js +5 -3
  15. package/src/rules/internals/angular-io.js +12 -13
  16. package/src/rules/internals/angular-member-visibility.js +11 -7
  17. package/src/rules/internals/import-resolution.js +191 -0
  18. package/src/rules/no-angular-router-api.js +23 -29
  19. package/src/rules/no-angular-seo-services.js +14 -9
  20. package/src/rules/no-cdk-import.js +7 -14
  21. package/src/rules/no-csp-unsafe.js +362 -0
  22. package/src/rules/no-document-cookie.js +3 -0
  23. package/src/rules/no-effect-cleanup-return.js +7 -3
  24. package/src/rules/no-empty-angular-metadata-arrays.js +3 -17
  25. package/src/rules/no-inject-chain.js +3 -1
  26. package/src/rules/no-leading-underscore-class-member.js +26 -3
  27. package/src/rules/no-legacy-angular-decorators.js +9 -21
  28. package/src/rules/no-legacy-query-import.js +6 -15
  29. package/src/rules/no-locale-id.js +4 -4
  30. package/src/rules/no-member-alias.js +11 -7
  31. package/src/rules/no-native-html-input-name.js +1 -1
  32. package/src/rules/no-pipe-logic.js +10 -17
  33. package/src/rules/no-redundant-on-push-change-detection.js +9 -23
  34. package/src/rules/no-rxjs-in-effect.js +12 -8
  35. package/src/rules/no-screaming-case-local.js +1 -0
  36. package/src/rules/no-standalone-flag.js +3 -17
  37. package/src/rules/no-subscribe-in-pipe.js +3 -2
  38. package/src/rules/no-template-literal-before-inline-template.js +3 -14
  39. package/src/rules/no-trivial-return-type.js +1 -0
  40. package/src/rules/no-trivial-wrapper-method.js +10 -16
  41. package/src/rules/no-typed-injected-element-ref.js +14 -17
  42. package/src/rules/no-unused-class-member.js +6 -9
  43. package/src/rules/prefer-clone-equal.js +8 -6
  44. package/src/rules/prefer-concise-angular-host-directives.js +23 -29
  45. package/src/rules/prefer-concise-angular-style-metadata.js +8 -16
  46. package/src/rules/prefer-element-dimensions.js +9 -10
  47. package/src/rules/prefer-linked-signal.js +42 -41
  48. package/src/rules/prefer-match-media.js +10 -14
  49. package/src/rules/prefer-present-tense-output.js +1 -1
  50. package/src/rules/prefer-rxjs-timer.js +3 -1
  51. package/src/rules/require-dollar-suffix.js +7 -4
  52. package/src/rules/require-on-push-change-detection.js +17 -25
  53. package/src/rules/require-view-encapsulation-none.js +17 -25
  54. package/src/rules/take-until-destroyed-last.js +64 -0
  55. package/src/rules/template-member-accessibility.js +8 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-next.25
4
+
5
+ ### Minor Changes
6
+
7
+ - The `@angular-eslint/*` output rules moved from `recommendedTs` into a new `recommendedAngularTs` config, which `recommended` includes, so NestJS and other non-Angular projects can use `recommendedTs` without the `@angular-eslint` plugin.
8
+ - `recommendedTs` now uses `ethlete/consistent-type-definitions` instead of `@typescript-eslint/consistent-type-definitions`, so `--fix` no longer turns an interface inside `declare module` or `declare global` (such as the theme-name registry the `@ethlete/core` generators emit) into a type alias that merges into nothing.
9
+ - New `no-csp-unsafe` rule (error in `recommendedTs` and `recommendedTemplate`) flags style attributes, un-nonced `<script>`/`<style>` elements, `eval` and string timers that a strict CSP blocks.
10
+ - `et update` turns the type aliases the old `consistent-type-definitions` fix left in `declare module` and `declare global` blocks back into interfaces, and renames the rule in `eslint-disable` directives.
11
+ - `prefer-linked-signal` now also reports an `effect()` that only calls `.set()` behind an `if`/`else`.
12
+
13
+ ### Patch Changes
14
+
15
+ - `ethlete/no-csp-unsafe` fixes a static template `style="…"` attribute. It rewrites `style="width: 40%; margin-top: 4px"` to `[style]="{ width: '40%', 'margin-top': '4px' }"`, which a strict CSP allows. A value with a quote, `url(`, `!important` or a comment stays a manual fix.
16
+
17
+ ## 1.0.0-next.24
18
+
19
+ ### Minor Changes
20
+
21
+ - Add `take-until-destroyed-last` to the recommended config: `takeUntilDestroyed()` must be the last operator in a `.pipe()`, since an operator after it can keep the subscription alive.
22
+
23
+ ### Patch Changes
24
+
25
+ - `class-member-order` now fixes classes with `static` members or index signatures and keeps a trailing comment with its member, and `prefer-concise-angular-host-directives` no longer drops or misplaces comments.
26
+ - Angular and RxJS rules now resolve `Component`, `inject`, `input`, `effect` and friends through their imports, `document` and `setTimeout` match only the globals, and import bans also report re-exports and dynamic imports.
27
+ - Several fixers no longer emit broken code (a dangling `other._value`, a duplicate `styleUrl`, a dropped `host` spread), `no-trivial-return-type` checks function declarations, `no-screaming-case-local` reports a function once, and `standalone` sorts after `selector`.
28
+
3
29
  ## 1.0.0-next.23
4
30
 
5
31
  ### Minor Changes
@@ -0,0 +1,12 @@
1
+ {
2
+ "migrations": [
3
+ {
4
+ "name": "module-augmentation-interfaces",
5
+ "version": "1.0.0-next.25",
6
+ "kind": "auto",
7
+ "description": "Turn the type aliases the old consistent-type-definitions fix left in declare module / declare global blocks back into interfaces, and rename its eslint-disable directives to ethlete/consistent-type-definitions.",
8
+ "generator": "@ethlete/eslint-plugin:restore-module-augmentation-interfaces",
9
+ "docs": "/eslint/rules"
10
+ }
11
+ ]
12
+ }
package/package.json CHANGED
@@ -1,14 +1,24 @@
1
1
  {
2
2
  "name": "@ethlete/eslint-plugin",
3
- "version": "1.0.0-next.23",
3
+ "version": "1.0.0-next.25",
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
9
  "@angular/core": ">=21.0.0",
10
+ "@nx/devkit": ">=21.0.0",
10
11
  "@typescript-eslint/eslint-plugin": ">=8.0.0",
11
12
  "eslint": ">=9.0.0",
12
13
  "typescript": ">=5.9.0"
13
- }
14
- }
14
+ },
15
+ "peerDependenciesMeta": {
16
+ "@nx/devkit": {
17
+ "optional": true
18
+ }
19
+ },
20
+ "ethlete": {
21
+ "migrations": "./migrations.json"
22
+ },
23
+ "generators": "./src/generators/generators.json"
24
+ }
@@ -15,8 +15,8 @@ const recommendedTs = {
15
15
  rules: {
16
16
  // ── TypeScript ──────────────────────────────────────────────────────────
17
17
 
18
- // No interface — use type
19
- '@typescript-eslint/consistent-type-definitions': ['error', 'type'],
18
+ // No interface — use type (owned by ethlete/consistent-type-definitions)
19
+ '@typescript-eslint/consistent-type-definitions': 'off',
20
20
 
21
21
  // No any
22
22
  '@typescript-eslint/no-explicit-any': 'error',
@@ -239,12 +239,18 @@ const recommendedTs = {
239
239
  // No .subscribe() inside a .pipe() callback
240
240
  'ethlete/no-subscribe-in-pipe': 'error',
241
241
 
242
+ // takeUntilDestroyed() is the last operator in a .pipe()
243
+ 'ethlete/take-until-destroyed-last': 'error',
244
+
242
245
  // No async/await — asynchronous work is modelled as cold Observables
243
246
  'ethlete/no-async-await': 'error',
244
247
 
245
248
  // No enum / const enum — use a const object with `as const` plus a derived union type
246
249
  'ethlete/no-enum': 'error',
247
250
 
251
+ // No interface — use type; an interface augmenting a module or the global scope is exempt
252
+ 'ethlete/consistent-type-definitions': 'error',
253
+
248
254
  // No .subscribe() inside effect() or computed()
249
255
  'ethlete/no-rxjs-in-effect': 'error',
250
256
 
@@ -308,6 +314,10 @@ const recommendedTs = {
308
314
  // — use getCookie / setCookie / hasCookie / deleteCookie from '@ethlete/core' instead
309
315
  'ethlete/no-document-cookie': 'error',
310
316
 
317
+ // No code a strict CSP (no 'unsafe-inline' / 'unsafe-eval') blocks: style attributes,
318
+ // un-nonced <script>/<style> elements, eval / new Function / string timers
319
+ 'ethlete/no-csp-unsafe': 'error',
320
+
311
321
  // Injected providers must be private by default, and protected only when
312
322
  // referenced from an Angular template or host binding.
313
323
  'ethlete/inject-member-accessibility': 'error',
@@ -347,14 +357,6 @@ const recommendedTs = {
347
357
  // Angular language service completions for the rest of the file
348
358
  'ethlete/no-template-literal-before-inline-template': 'error',
349
359
 
350
- // ── Angular outputs ─────────────────────────────────────────────────────
351
-
352
- // No on-prefixed outputs (onSelectDate → selectDate)
353
- '@angular-eslint/no-output-on-prefix': 'error',
354
-
355
- // No outputs named after native DOM events (change, click, etc.)
356
- '@angular-eslint/no-output-native': 'error',
357
-
358
360
  // ── Angular components ──────────────────────────────────────────────────
359
361
 
360
362
  // Require ChangeDetectionStrategy.OnPush on Angular <= 21, where it is opt-in
@@ -399,6 +401,23 @@ const recommendedTs = {
399
401
  },
400
402
  };
401
403
 
404
+ /**
405
+ * `@angular-eslint/*` rules for TypeScript files in Angular projects; the base config must register
406
+ * the `@angular-eslint` plugin.
407
+ *
408
+ * @type {import('eslint').Linter.Config}
409
+ */
410
+ const recommendedAngularTs = {
411
+ files: ['**/*.ts'],
412
+ rules: {
413
+ // No on-prefixed outputs (onSelectDate → selectDate)
414
+ '@angular-eslint/no-output-on-prefix': 'error',
415
+
416
+ // No outputs named after native DOM events (change, click, etc.)
417
+ '@angular-eslint/no-output-native': 'error',
418
+ },
419
+ };
420
+
402
421
  /**
403
422
  * Flat config entries for Angular HTML template files.
404
423
  * @type {import('eslint').Linter.Config}
@@ -416,6 +435,8 @@ const recommendedTemplate = {
416
435
  'ethlete/prefer-static-boolean-properties': 'warn',
417
436
  // A form must handle its own submission
418
437
  'ethlete/require-form-submit': 'error',
438
+ // No static style attribute or [attr.style] binding - a strict CSP blocks both
439
+ 'ethlete/no-csp-unsafe': 'error',
419
440
  },
420
441
  };
421
442
 
@@ -429,6 +450,7 @@ const recommendedSpec = {
429
450
  '@typescript-eslint/no-non-null-assertion': 'off',
430
451
  'no-restricted-globals': 'off',
431
452
  'ethlete/no-async-await': 'off',
453
+ 'ethlete/no-csp-unsafe': 'off',
432
454
  'ethlete/no-direct-dom-manipulation': 'off',
433
455
  'ethlete/no-document-cookie': 'off',
434
456
  'ethlete/no-dom-query': 'off',
@@ -441,4 +463,4 @@ const recommendedSpec = {
441
463
  },
442
464
  };
443
465
 
444
- module.exports = { recommendedTs, recommendedTemplate, recommendedSpec };
466
+ module.exports = { recommendedTs, recommendedAngularTs, recommendedTemplate, recommendedSpec };
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nx-generators",
3
+ "generators": {
4
+ "restore-module-augmentation-interfaces": {
5
+ "implementation": "./restore-module-augmentation-interfaces/migration",
6
+ "schema": "./restore-module-augmentation-interfaces/schema.json",
7
+ "description": "Turn type aliases in declare module / declare global blocks back into interfaces, and rename consistent-type-definitions eslint directives"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,50 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const { formatFiles, visitNotIgnoredFiles } = require('@nx/devkit');
5
+ const { restoreModuleAugmentationInterfaces } = require('./restore-module-augmentation-interfaces');
6
+
7
+ const SOURCE_FILE = /\.(?:[cm]?ts|tsx|[cm]?js|jsx)$/;
8
+
9
+ /**
10
+ * @param {import('@nx/devkit').Tree} tree
11
+ * @param {{ skipFormat?: boolean }} schema
12
+ */
13
+ const migrateModuleAugmentationInterfaces = async (tree, schema) => {
14
+ console.log('\n🔄 Restoring module augmentation interfaces...');
15
+
16
+ let filesChanged = 0;
17
+ let reviewCount = 0;
18
+
19
+ visitNotIgnoredFiles(tree, '', (filePath) => {
20
+ if (!SOURCE_FILE.test(filePath)) return;
21
+
22
+ const before = tree.read(filePath, 'utf-8');
23
+
24
+ if (!before) return;
25
+
26
+ const result = restoreModuleAugmentationInterfaces(filePath, before);
27
+
28
+ for (const entry of result.review) {
29
+ reviewCount++;
30
+ console.warn(
31
+ ` ⚠ ${filePath}:${entry.line}: type ${entry.name} in a module augmentation is not an object literal, so it was left alone. An augmentation only merges as an interface.`,
32
+ );
33
+ }
34
+
35
+ if (!result.changed) return;
36
+
37
+ tree.write(filePath, result.content);
38
+ filesChanged++;
39
+ console.log(` ✓ ${filePath}`);
40
+ });
41
+
42
+ console.log(filesChanged === 0 ? ' Nothing to migrate.\n' : `\n✅ Migrated ${filesChanged} file(s).\n`);
43
+
44
+ if (reviewCount > 0) console.warn(` Review ${reviewCount} type alias(es) in module augmentations manually.\n`);
45
+
46
+ if (!schema.skipFormat) await formatFiles(tree);
47
+ };
48
+
49
+ module.exports = migrateModuleAugmentationInterfaces;
50
+ module.exports.default = migrateModuleAugmentationInterfaces;
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+ 'use strict';
3
+
4
+ const ts = require('typescript');
5
+
6
+ const OLD_RULE = '@typescript-eslint/consistent-type-definitions';
7
+ const NEW_RULE = 'ethlete/consistent-type-definitions';
8
+
9
+ const LINE_DIRECTIVE = /\/\/[ \t]*eslint-(?:disable|enable)(?:-next-line|-line)?\b[^\n]*/g;
10
+ const BLOCK_DIRECTIVE = /\/\*[ \t]*eslint-(?:disable|enable)(?:-next-line|-line)?\b(?:(?!\*\/)[\s\S])*\*\//g;
11
+
12
+ const AUGMENTATION = /\bdeclare\s+(?:module|global)\b/;
13
+
14
+ const renameInDirective = (/** @type {string} */ directive) => directive.split(OLD_RULE).join(NEW_RULE);
15
+
16
+ /** Renames `@typescript-eslint/consistent-type-definitions` in every eslint-disable/enable directive. */
17
+ const renameDirectives = (/** @type {string} */ content) =>
18
+ content.replace(LINE_DIRECTIVE, renameInDirective).replace(BLOCK_DIRECTIVE, renameInDirective);
19
+
20
+ const isAugmentation = (/** @type {ts.Statement} */ statement) =>
21
+ ts.isModuleDeclaration(statement) &&
22
+ (ts.isStringLiteral(statement.name) || (statement.flags & ts.NodeFlags.GlobalAugmentation) !== 0) &&
23
+ statement.body !== undefined &&
24
+ ts.isModuleBlock(statement.body);
25
+
26
+ /**
27
+ * @param {readonly ts.Statement[]} statements
28
+ * @param {ts.TypeAliasDeclaration[]} found
29
+ */
30
+ const collectAugmentationAliases = (statements, found) => {
31
+ for (const statement of statements) {
32
+ if (!isAugmentation(statement)) continue;
33
+
34
+ const body = /** @type {ts.ModuleBlock} */ (/** @type {ts.ModuleDeclaration} */ (statement).body);
35
+
36
+ for (const member of body.statements) {
37
+ if (ts.isTypeAliasDeclaration(member)) found.push(member);
38
+ }
39
+
40
+ collectAugmentationAliases(body.statements, found);
41
+ }
42
+
43
+ return found;
44
+ };
45
+
46
+ /**
47
+ * Turns every object-literal `type X = { … }` directly inside a `declare module '…'` or
48
+ * `declare global` block back into `interface X { … }`, and renames
49
+ * `@typescript-eslint/consistent-type-definitions` in eslint directives to
50
+ * `ethlete/consistent-type-definitions`. Aliases of any other shape are returned in `review`.
51
+ *
52
+ * @param {string} filePath
53
+ * @param {string} content
54
+ * @returns {{ content: string; changed: boolean; review: { name: string; line: number }[] }}
55
+ */
56
+ const restoreModuleAugmentationInterfaces = (filePath, content) => {
57
+ if (!AUGMENTATION.test(content)) {
58
+ const renamed = renameDirectives(content);
59
+
60
+ return { content: renamed, changed: renamed !== content, review: [] };
61
+ }
62
+
63
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
64
+ const aliases = collectAugmentationAliases(sourceFile.statements, []);
65
+
66
+ /** @type {{ name: string; line: number }[]} */
67
+ const review = [];
68
+ /** @type {{ start: number; end: number; text: string }[]} */
69
+ const edits = [];
70
+
71
+ for (const alias of aliases) {
72
+ const name = alias.name.text;
73
+
74
+ if (!ts.isTypeLiteralNode(alias.type)) {
75
+ review.push({ name, line: sourceFile.getLineAndCharacterOfPosition(alias.getStart(sourceFile)).line + 1 });
76
+ continue;
77
+ }
78
+
79
+ const modifiers = (alias.modifiers ?? []).map((modifier) => `${modifier.getText(sourceFile)} `).join('');
80
+ const typeParameters = content
81
+ .slice(alias.name.end, alias.type.pos)
82
+ .replace(/\s*=\s*$/, '')
83
+ .trim();
84
+
85
+ edits.push({
86
+ start: alias.modifiers?.[0]?.getStart(sourceFile) ?? alias.getStart(sourceFile),
87
+ end: alias.end,
88
+ text: `${modifiers}interface ${name}${typeParameters} ${alias.type.getText(sourceFile)}`,
89
+ });
90
+ }
91
+
92
+ let next = content;
93
+
94
+ for (const edit of edits.sort((left, right) => right.start - left.start)) {
95
+ next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
96
+ }
97
+
98
+ next = renameDirectives(next);
99
+
100
+ return { content: next, changed: next !== content, review };
101
+ };
102
+
103
+ module.exports = { restoreModuleAugmentationInterfaces };
@@ -0,0 +1,14 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "restore-module-augmentation-interfaces",
4
+ "title": "Restore module augmentation interfaces",
5
+ "type": "object",
6
+ "properties": {
7
+ "skipFormat": {
8
+ "type": "boolean",
9
+ "description": "Skip formatting files after migration",
10
+ "default": false
11
+ }
12
+ },
13
+ "required": []
14
+ }
package/src/index.js CHANGED
@@ -30,6 +30,7 @@ const noLegacyAngularDecorators = require('./rules/no-legacy-angular-decorators'
30
30
  const noAngularSeoServices = require('./rules/no-angular-seo-services');
31
31
  const preferCloneEqual = require('./rules/prefer-clone-equal');
32
32
  const noDocumentCookie = require('./rules/no-document-cookie');
33
+ const noCspUnsafe = require('./rules/no-csp-unsafe');
33
34
  const injectMemberAccessibility = require('./rules/inject-member-accessibility');
34
35
  const noRedundantInternal = require('./rules/no-redundant-internal');
35
36
  const templateMemberAccessibility = require('./rules/template-member-accessibility');
@@ -60,7 +61,9 @@ const noCdkImport = require('./rules/no-cdk-import');
60
61
  const noLegacyQueryImport = require('./rules/no-legacy-query-import');
61
62
  const noAsyncAwait = require('./rules/no-async-await');
62
63
  const noEnum = require('./rules/no-enum');
63
- const { recommendedTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
64
+ const consistentTypeDefinitions = require('./rules/consistent-type-definitions');
65
+ const takeUntilDestroyedLast = require('./rules/take-until-destroyed-last');
66
+ const { recommendedTs, recommendedAngularTs, recommendedTemplate, recommendedSpec } = require('./configs/recommended');
64
67
  const { version } = require('../package.json');
65
68
 
66
69
  /** @type {import('eslint').ESLint.Plugin} */
@@ -74,6 +77,7 @@ const plugin = {
74
77
  'no-legacy-prepare-without-injector': noLegacyPrepareWithoutInjector,
75
78
  'no-trivial-return-type': noTrivialReturnType,
76
79
  'no-subscribe-in-pipe': noSubscribeInPipe,
80
+ 'take-until-destroyed-last': takeUntilDestroyedLast,
77
81
  'no-rxjs-in-effect': noRxjsInEffect,
78
82
  'no-effect-cleanup-return': noEffectCleanupReturn,
79
83
  'no-readonly-signal': noReadonlySignal,
@@ -100,6 +104,7 @@ const plugin = {
100
104
  'no-angular-seo-services': noAngularSeoServices,
101
105
  'prefer-clone-equal': preferCloneEqual,
102
106
  'no-document-cookie': noDocumentCookie,
107
+ 'no-csp-unsafe': noCspUnsafe,
103
108
  'inject-member-accessibility': injectMemberAccessibility,
104
109
  'no-redundant-internal': noRedundantInternal,
105
110
  'template-member-accessibility': templateMemberAccessibility,
@@ -129,6 +134,7 @@ const plugin = {
129
134
  'no-legacy-query-import': noLegacyQueryImport,
130
135
  'no-async-await': noAsyncAwait,
131
136
  'no-enum': noEnum,
137
+ 'consistent-type-definitions': consistentTypeDefinitions,
132
138
  },
133
139
  };
134
140
 
@@ -148,6 +154,8 @@ const recommendedTemplateWithPlugin = {
148
154
  const configs = {
149
155
  /** TypeScript rules (merged into a files: ['**\/*.ts'] block) */
150
156
  recommendedTs: recommendedTsWithPlugin,
157
+ /** `@angular-eslint/*` rules for TypeScript files (files: ['**\/*.ts']) */
158
+ recommendedAngularTs,
151
159
  /** Angular template rules (merged into a files: ['**\/*.html'] block) */
152
160
  recommendedTemplate: recommendedTemplateWithPlugin,
153
161
  /** Relaxed rules for spec files (files: ['**\/*.spec.ts']) */
@@ -156,7 +164,7 @@ const configs = {
156
164
  * Both together as a flat array — the most common usage:
157
165
  * export default [...baseConfig, ...ethlete.configs.recommended]
158
166
  */
159
- recommended: [recommendedTsWithPlugin, recommendedTemplateWithPlugin, recommendedSpec],
167
+ recommended: [recommendedTsWithPlugin, recommendedAngularTs, recommendedTemplateWithPlugin, recommendedSpec],
160
168
  };
161
169
 
162
170
  const ethletePlugin = { ...plugin, configs };
@@ -1,6 +1,8 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  /** @typedef {'Component' | 'Directive'} TDecoratorName */
5
7
  /** @typedef {{ order: string[]; orderText: string }} TDecoratorConfig */
6
8
  /** @typedef {{ range: [number, number] }} TNodeWithRange */
@@ -14,11 +16,13 @@ const DECORATOR_CONFIG = {
14
16
  Component: {
15
17
  order: [
16
18
  'selector',
19
+ 'standalone',
17
20
  'template',
18
21
  'styleUrl',
19
22
  'encapsulation',
20
23
  'changeDetection',
21
24
  'imports',
25
+ 'deferredImports',
22
26
  'providers',
23
27
  'viewProviders',
24
28
  'animations',
@@ -29,16 +33,28 @@ const DECORATOR_CONFIG = {
29
33
  'outputs',
30
34
  'queries',
31
35
  'preserveWhitespaces',
36
+ 'interpolation',
32
37
  'schemas',
33
38
  'jit',
34
39
  'exportAs',
35
40
  ],
36
41
  orderText:
37
- 'selector, template/templateUrl, styleUrl/styleUrls, encapsulation, changeDetection, imports, providers, viewProviders, animations, hostDirectives, host, styles, inputs, outputs, queries, preserveWhitespaces, schemas, jit, exportAs',
42
+ 'selector, standalone, template/templateUrl, styleUrl/styleUrls, encapsulation, changeDetection, imports, deferredImports, providers, viewProviders, animations, hostDirectives, host, styles, inputs, outputs, queries, preserveWhitespaces, interpolation, schemas, jit, exportAs',
38
43
  },
39
44
  Directive: {
40
- order: ['selector', 'exportAs', 'providers', 'inputs', 'outputs', 'queries', 'hostDirectives', 'host', 'jit'],
41
- orderText: 'selector, exportAs, providers, inputs, outputs, queries, hostDirectives, host, jit',
45
+ order: [
46
+ 'selector',
47
+ 'standalone',
48
+ 'exportAs',
49
+ 'providers',
50
+ 'inputs',
51
+ 'outputs',
52
+ 'queries',
53
+ 'hostDirectives',
54
+ 'host',
55
+ 'jit',
56
+ ],
57
+ orderText: 'selector, standalone, exportAs, providers, inputs, outputs, queries, hostDirectives, host, jit',
42
58
  },
43
59
  };
44
60
 
@@ -66,22 +82,6 @@ const getOrderKey = (decoratorName, propertyName) => {
66
82
  return propertyName;
67
83
  };
68
84
 
69
- /**
70
- * @param {import('eslint').Rule.Node} node
71
- * @returns {TDecoratorName | null}
72
- */
73
- const getDecoratorName = (node) => {
74
- const decorator = /** @type {any} */ (node);
75
- if (decorator.type !== 'Decorator') return null;
76
-
77
- const expression = decorator.expression;
78
- if (expression.type === 'CallExpression') {
79
- return expression.callee.type === 'Identifier' ? expression.callee.name : null;
80
- }
81
-
82
- return expression.type === 'Identifier' ? expression.name : null;
83
- };
84
-
85
85
  /** @type {import('eslint').Rule.RuleModule} */
86
86
  const angularDecoratorPropertyOrder = {
87
87
  meta: {
@@ -101,7 +101,7 @@ const angularDecoratorPropertyOrder = {
101
101
  return {
102
102
  /** @param {import('eslint').Rule.Node} node */
103
103
  Decorator(node) {
104
- const decoratorName = getDecoratorName(node);
104
+ const decoratorName = getAngularDecoratorName(context.sourceCode, node);
105
105
  if (decoratorName !== 'Component' && decoratorName !== 'Directive') return;
106
106
 
107
107
  const decorator = /** @type {any} */ (node);
@@ -1,24 +1,18 @@
1
1
  // @ts-check
2
2
  'use strict';
3
3
 
4
+ const { getAngularDecoratorName } = require('./internals/import-resolution');
5
+
4
6
  const SCREAMING_CASE_RE = /^[A-Z][A-Z0-9_]*$/;
5
7
 
6
8
  /**
9
+ * @param {import('eslint').SourceCode} sourceCode
7
10
  * @param {any} classNode
8
11
  */
9
- const hasPipeDecorator = (classNode) => {
10
- const decorators = classNode.decorators ?? [];
11
- return decorators.some((/** @type {any} */ dec) => {
12
- const expr = dec.expression;
13
- if (expr.type === 'CallExpression') {
14
- return expr.callee.type === 'Identifier' && expr.callee.name === 'Pipe';
15
- }
16
- if (expr.type === 'Identifier') {
17
- return expr.name === 'Pipe';
18
- }
19
- return false;
20
- });
21
- };
12
+ const hasPipeDecorator = (sourceCode, classNode) =>
13
+ (classNode.decorators ?? []).some(
14
+ (/** @type {any} */ decorator) => getAngularDecoratorName(sourceCode, decorator) === 'Pipe',
15
+ );
22
16
 
23
17
  const REACTIVE_APIS = new Set([
24
18
  'signal',
@@ -353,7 +347,7 @@ const classConstantProperty = {
353
347
  continue;
354
348
  }
355
349
 
356
- if (memberName === 'transform' && hasPipeDecorator(node.parent)) {
350
+ if (memberName === 'transform' && hasPipeDecorator(context.sourceCode, node.parent)) {
357
351
  continue;
358
352
  }
359
353