Skip to content

Commit ea87c86

Browse files
fix: compatibility with eslint/config's defineConfig (#283)
Currently there is an incompatibility between eslint's types and tslint's, especially since we use tseslint's RuleCreator (see typescript-eslint/typescript-eslint#11543). - Fixed compatibility with `defineConfig()` so it works in addition to the deprecated `tseslint.config()`. - The plugin now satisfies `ESLint.Plugin` instead of `TSESLint.FlatConfig.Plugin`. - The configs now satisfy `Linter.Config` instead of `TSESLint.FlatConfig.Config`. - The rules satisfy _both_ `Rule.RuleModule` and `TSESLint.RuleModule`. - Improved the compiled types to include inlined literal docs (e.g. `description` in intellisense is the actual rule description instead of just `string`). - Internally upgraded our own eslint config to `defineConfig`. - Bumped the vitest eslint plugin to 1.4.0 to be compatible with defineConfig. - Pinned `@eslint/config-helpers` because 0.4.2 causes type errors. - Removed references to `tseslint.config` from the README. - Re-wrote the migration section of the README to include exact steps required. Resolves #282
1 parent f0d4a3d commit ea87c86

File tree

9 files changed

+160
-105
lines changed

9 files changed

+160
-105
lines changed

README.md

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,70 @@ This ESLint plugin is intended to prevent issues with [RxJS](https://github.com/
77

88
Most of these rules require TypeScript typed linting and are indicated as such below.
99

10-
## Migrating from `eslint-plugin-rxjs`
10+
## Migration Guide from `eslint-plugin-rxjs`
1111

12-
This project is a fork of [`eslint-plugin-rxjs`](https://github.com/cartant/eslint-plugin-rxjs)
13-
initially started to support the new ESLint flat config format.
14-
There are some breaking changes:
12+
This project started as a fork of [`eslint-plugin-rxjs`](https://github.com/cartant/eslint-plugin-rxjs)
13+
but has since introduced new features which involve breaking changes.
1514

16-
- The old `.eslintrc` format is not supported.
17-
- If you need to continue using this old format, use the original `eslint-plugin-rxjs` or a different fork.
18-
- The plugin namespace specified in the `recommended` config was changed from `rxjs` to `rxjs-x`.
19-
- e.g. In your ESLint config, `rxjs/no-subject-value` should be renamed to `rxjs-x/no-subject-value`.
20-
- The rule `rxjs/no-ignored-observable` is renamed to `rxjs-x/no-floating-observables`.
15+
1. Migrate your config from the old `.eslintrc` format to `eslint.config.mjs` (or similar), and uninstall `eslint-plugin-rxjs`.
16+
- See ESLint's guide here: [https://eslint.org/docs/latest/use/configure/migration-guide].
17+
- If you need to continue using the deprecated format, use the original `eslint-plugin-rxjs` or a different fork.
18+
2. Install `eslint-plugin-rxjs-x`, and import it into your config.
2119

22-
A complete description of all changes are documented in the [CHANGELOG](CHANGELOG.md) file.
20+
```diff
21+
+ import rxjsX from 'eslint-plugin-rxjs-x';
22+
```
2323

24-
## Install
24+
3. If you previously used the `plugin:rxjs/recommended` shared config, add `rxjsX.configs.recommended` to your `configs` block:
25+
26+
```diff
27+
configs: {
28+
+ rxjsX.configs.recommended,
29+
},
30+
```
31+
32+
- Note: `eslint-plugin-rxjs-x` provides a `strict` shared config, so consider using `rxjsX.configs.strict` instead.
33+
4. If you previously did _not_ use a shared config, add the plugin to your `plugins` block with the new namespace:
34+
35+
```diff
36+
plugins: {
37+
+ 'rxjs-x': rxjsX,
38+
},
39+
```
40+
41+
- Note: this is unnecessary if you are using a shared config.
42+
5. In your `rules` block, replace the namespace `rxjs` with `rxjs-x`:
43+
44+
```diff
45+
- 'rxjs/no-subject-value': 'error',
46+
+ 'rxjs-x/no-subject-value': 'error',
47+
```
48+
49+
6. If you previously used `rxjs/no-ignored-observable`, replace it with `rxjs-x/no-floating-observables`.
50+
51+
```diff
52+
- 'rxjs/no-ignored-observable': 'error',
53+
+ 'rxjs-x/no-floating-observables': 'error',
54+
```
55+
56+
> [!TIP]
57+
> A complete description of all changes are documented in the [CHANGELOG](CHANGELOG.md) file.
58+
59+
## Installation Guide
2560

2661
See [typescript-eslint's Getting Started](https://typescript-eslint.io/getting-started) for a full ESLint setup guide.
2762

28-
Then use the `recommended` configuration in your `eslint.config.mjs` and enable typed linting:
63+
1. Enable typed linting.
64+
- See [Linting with Type Information](https://typescript-eslint.io/getting-started/typed-linting/) for more information.
65+
2. Start with the `recommended` shared config in your `eslint.config.mjs`.
2966

3067
```js
3168
// @ts-check
69+
import { defineConfig } from 'eslint/config';
3270
import tseslint from 'typescript-eslint';
3371
import rxjsX from 'eslint-plugin-rxjs-x';
3472

35-
export default tseslint.config({
73+
export default defineConfig({
3674
extends: [
3775
...tseslint.configs.recommended,
3876
rxjsX.configs.recommended,
@@ -45,10 +83,6 @@ export default tseslint.config({
4583
});
4684
```
4785

48-
The above example uses `typescript-eslint`'s built-in config to set up the TypeScript parser for us.
49-
Enabling `projectService` then turns on typed linting.
50-
See [Linting with Type Information](https://typescript-eslint.io/getting-started/typed-linting/) for details.
51-
5286
## Configs
5387

5488
<!-- begin auto-generated configs list -->

eslint.config.mjs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// @ts-check
22
import js from '@eslint/js';
33
import stylistic from '@stylistic/eslint-plugin';
4+
import vitest from '@vitest/eslint-plugin';
45
import gitignore from 'eslint-config-flat-gitignore';
6+
import eslintPlugin from 'eslint-plugin-eslint-plugin';
57
import importX from 'eslint-plugin-import-x';
68
import n from 'eslint-plugin-n';
9+
import { defineConfig } from 'eslint/config';
710
import tseslint from 'typescript-eslint';
8-
import vitest from '@vitest/eslint-plugin';
9-
import eslintPlugin from 'eslint-plugin-eslint-plugin';
1011

11-
export default tseslint.config(gitignore(), {
12+
export default defineConfig(gitignore(), {
1213
files: [
1314
'src/**/*.ts',
1415
'tests/**/*.ts',

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,15 @@
7272
}
7373
},
7474
"devDependencies": {
75+
"@eslint/config-helpers": "0.4.1",
7576
"@eslint/js": "^9.38.0",
7677
"@stylistic/eslint-plugin": "^5.5.0",
7778
"@types/common-tags": "^1.8.4",
7879
"@types/node": "~18.18.0",
7980
"@typescript-eslint/rule-tester": "^8.46.2",
8081
"@typescript/vfs": "^1.6.1",
8182
"@vitest/coverage-v8": "^3.2.4",
82-
"@vitest/eslint-plugin": "^1.2.7",
83+
"@vitest/eslint-plugin": "^1.4.0",
8384
"bumpp": "^10.3.1",
8485
"eslint": "^9.38.0",
8586
"eslint-config-flat-gitignore": "^2.1.0",

src/configs/recommended.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
1+
import type { ESLint, Linter } from 'eslint';
22

33
export const createRecommendedConfig = (
4-
plugin: TSESLint.FlatConfig.Plugin,
4+
plugin: ESLint.Plugin,
55
) => ({
66
name: 'rxjs-x/recommended' as const,
77
plugins: {
8-
'rxjs-x': plugin,
8+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- "A type annotation is necessary."
9+
'rxjs-x': plugin as ESLint.Plugin,
910
},
1011
rules: {
1112
'rxjs-x/no-async-subscribe': 'error',
@@ -29,4 +30,4 @@ export const createRecommendedConfig = (
2930
'rxjs-x/prefer-root-operators': 'error',
3031
'rxjs-x/throw-error': 'error',
3132
},
32-
} satisfies TSESLint.FlatConfig.Config);
33+
} satisfies Linter.Config);

src/configs/strict.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
1+
import type { ESLint, Linter } from 'eslint';
22

33
export const createStrictConfig = (
4-
plugin: TSESLint.FlatConfig.Plugin,
4+
plugin: ESLint.Plugin,
55
) => ({
66
name: 'rxjs-x/strict' as const,
77
plugins: {
8-
'rxjs-x': plugin,
8+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- "A type annotation is necessary."
9+
'rxjs-x': plugin as ESLint.Plugin,
910
},
1011
rules: {
1112
'rxjs-x/no-async-subscribe': 'error',
@@ -40,4 +41,4 @@ export const createStrictConfig = (
4041
allowThrowingUnknown: false as const,
4142
}],
4243
},
43-
} satisfies TSESLint.FlatConfig.Config);
44+
} satisfies Linter.Config);

src/index.ts

Lines changed: 54 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { TSESLint } from '@typescript-eslint/utils';
1+
import type { TSESLint } from '@typescript-eslint/utils';
2+
import type { ESLint, Rule } from 'eslint';
23
import { name, version } from '../package.json';
34
import { createRecommendedConfig } from './configs/recommended';
45
import { createStrictConfig } from './configs/strict';
@@ -49,63 +50,66 @@ import { preferRootOperatorsRule } from './rules/prefer-root-operators';
4950
import { suffixSubjectsRule } from './rules/suffix-subjects';
5051
import { throwErrorRule } from './rules/throw-error';
5152

53+
const allRules = {
54+
'ban-observables': banObservablesRule,
55+
'ban-operators': banOperatorsRule,
56+
'finnish': finnishRule,
57+
'just': justRule,
58+
'macro': macroRule,
59+
'no-async-subscribe': noAsyncSubscribeRule,
60+
'no-compat': noCompatRule,
61+
'no-connectable': noConnectableRule,
62+
'no-create': noCreateRule,
63+
'no-cyclic-action': noCyclicActionRule,
64+
'no-explicit-generics': noExplicitGenericsRule,
65+
'no-exposed-subjects': noExposedSubjectsRule,
66+
'no-finnish': noFinnishRule,
67+
'no-floating-observables': noFloatingObservablesRule,
68+
'no-ignored-default-value': noIgnoredDefaultValueRule,
69+
'no-ignored-error': noIgnoredErrorRule,
70+
'no-ignored-notifier': noIgnoredNotifierRule,
71+
'no-ignored-replay-buffer': noIgnoredReplayBufferRule,
72+
'no-ignored-subscribe': noIgnoredSubscribeRule,
73+
'no-ignored-subscription': noIgnoredSubscriptionRule,
74+
'no-ignored-takewhile-value': noIgnoredTakewhileValueRule,
75+
'no-implicit-any-catch': noImplicitAnyCatchRule,
76+
'no-index': noIndexRule,
77+
'no-internal': noInternalRule,
78+
'no-misused-observables': noMisusedObservablesRule,
79+
'no-nested-subscribe': noNestedSubscribeRule,
80+
'no-redundant-notify': noRedundantNotifyRule,
81+
'no-sharereplay': noSharereplayRule,
82+
'no-subclass': noSubclassRule,
83+
'no-subject-unsubscribe': noSubjectUnsubscribeRule,
84+
'no-subject-value': noSubjectValueRule,
85+
'no-subscribe-handlers': noSubscribeHandlersRule,
86+
'no-subscribe-in-pipe': noSubscribeInPipeRule,
87+
'no-tap': noTapRule,
88+
'no-topromise': noTopromiseRule,
89+
'no-unbound-methods': noUnboundMethodsRule,
90+
'no-unsafe-catch': noUnsafeCatchRule,
91+
'no-unsafe-first': noUnsafeFirstRule,
92+
'no-unsafe-subject-next': noUnsafeSubjectNext,
93+
'no-unsafe-switchmap': noUnsafeSwitchmapRule,
94+
'no-unsafe-takeuntil': noUnsafeTakeuntilRule,
95+
'prefer-observer': preferObserverRule,
96+
'prefer-root-operators': preferRootOperatorsRule,
97+
'suffix-subjects': suffixSubjectsRule,
98+
'throw-error': throwErrorRule,
99+
} satisfies TSESLint.FlatConfig.Plugin['rules'];
100+
52101
const plugin = {
53102
meta: { name, version },
54-
rules: {
55-
'ban-observables': banObservablesRule,
56-
'ban-operators': banOperatorsRule,
57-
'finnish': finnishRule,
58-
'just': justRule,
59-
'macro': macroRule,
60-
'no-async-subscribe': noAsyncSubscribeRule,
61-
'no-compat': noCompatRule,
62-
'no-connectable': noConnectableRule,
63-
'no-create': noCreateRule,
64-
'no-cyclic-action': noCyclicActionRule,
65-
'no-explicit-generics': noExplicitGenericsRule,
66-
'no-exposed-subjects': noExposedSubjectsRule,
67-
'no-finnish': noFinnishRule,
68-
'no-floating-observables': noFloatingObservablesRule,
69-
'no-ignored-default-value': noIgnoredDefaultValueRule,
70-
'no-ignored-error': noIgnoredErrorRule,
71-
'no-ignored-notifier': noIgnoredNotifierRule,
72-
'no-ignored-replay-buffer': noIgnoredReplayBufferRule,
73-
'no-ignored-subscribe': noIgnoredSubscribeRule,
74-
'no-ignored-subscription': noIgnoredSubscriptionRule,
75-
'no-ignored-takewhile-value': noIgnoredTakewhileValueRule,
76-
'no-implicit-any-catch': noImplicitAnyCatchRule,
77-
'no-index': noIndexRule,
78-
'no-internal': noInternalRule,
79-
'no-misused-observables': noMisusedObservablesRule,
80-
'no-nested-subscribe': noNestedSubscribeRule,
81-
'no-redundant-notify': noRedundantNotifyRule,
82-
'no-sharereplay': noSharereplayRule,
83-
'no-subclass': noSubclassRule,
84-
'no-subject-unsubscribe': noSubjectUnsubscribeRule,
85-
'no-subject-value': noSubjectValueRule,
86-
'no-subscribe-handlers': noSubscribeHandlersRule,
87-
'no-subscribe-in-pipe': noSubscribeInPipeRule,
88-
'no-tap': noTapRule,
89-
'no-topromise': noTopromiseRule,
90-
'no-unbound-methods': noUnboundMethodsRule,
91-
'no-unsafe-catch': noUnsafeCatchRule,
92-
'no-unsafe-first': noUnsafeFirstRule,
93-
'no-unsafe-subject-next': noUnsafeSubjectNext,
94-
'no-unsafe-switchmap': noUnsafeSwitchmapRule,
95-
'no-unsafe-takeuntil': noUnsafeTakeuntilRule,
96-
'prefer-observer': preferObserverRule,
97-
'prefer-root-operators': preferRootOperatorsRule,
98-
'suffix-subjects': suffixSubjectsRule,
99-
'throw-error': throwErrorRule,
100-
},
101-
} satisfies TSESLint.FlatConfig.Plugin;
103+
/** Compatibility with `defineConfig` until https://github.com/typescript-eslint/typescript-eslint/issues/11543 is addressed. */
104+
rules: allRules as { [K in keyof typeof allRules]: (typeof allRules)[K] & Rule.RuleModule },
105+
} satisfies ESLint.Plugin;
102106

103107
const rxjsX = {
104108
...plugin,
105109
configs: {
106110
recommended: createRecommendedConfig(plugin),
107111
strict: createStrictConfig(plugin),
108112
},
109-
} satisfies TSESLint.FlatConfig.Plugin;
113+
} satisfies ESLint.Plugin;
110114

111115
export default rxjsX;

src/utils/rule-creator.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
import { ESLintUtils, TSESLint } from '@typescript-eslint/utils';
22
import { version } from '../../package.json';
33

4-
export interface RxjsXRuleDocs {
5-
description: string;
6-
recommended?: TSESLint.RuleRecommendation | TSESLint.RuleRecommendationAcrossConfigs<unknown[]>;
4+
export interface RxjsXRuleDocs<Options extends readonly unknown[], Desc extends string> {
5+
description: Desc;
6+
recommended?: TSESLint.RuleRecommendation | TSESLint.RuleRecommendationAcrossConfigs<Options>;
77
requiresTypeChecking?: boolean;
88
}
99

1010
const REPO_URL = 'https://github.com/JasonWeinzierl/eslint-plugin-rxjs-x';
1111

12-
export const ruleCreator = ESLintUtils.RuleCreator<RxjsXRuleDocs>(
12+
export const ruleCreator = ESLintUtils.RuleCreator<RxjsXRuleDocs<unknown[], string>>(
1313
(name) =>
1414
`${REPO_URL}/blob/v${version}/docs/rules/${name}.md`,
15-
);
15+
// Ensure the resulting types are narrowed to exactly what each rule declares.
16+
) as <
17+
Options extends readonly unknown[],
18+
MessageIds extends string,
19+
Desc extends string,
20+
Docs extends RxjsXRuleDocs<Options, Desc>,
21+
>({ meta, name, ...rule }: Readonly<ESLintUtils.RuleWithMetaAndName<Options, MessageIds, Docs>>) => TSESLint.RuleModule<MessageIds, Options, Docs>;

tests/package.test.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ describe('package', () => {
5151
const namespace = 'rxjs-x';
5252
const fullRuleName = `${namespace}/${ruleName}`;
5353

54-
if (!rule.meta.docs?.recommended) {
54+
const ruleRec = rule.meta.docs?.recommended;
55+
56+
if (!ruleRec) {
5557
// Rule is not included in any configuration.
5658
expect(plugin.configs.recommended.rules).not.toHaveProperty(fullRuleName);
5759
expect(plugin.configs.strict.rules).not.toHaveProperty(fullRuleName);
58-
} else if (typeof rule.meta.docs.recommended === 'string') {
60+
} else if (typeof ruleRec === 'string') {
5961
// Rule specifies only a configuration name.
60-
expect(rule.meta.docs.recommended).toMatch(/^(recommended|strict)$/);
61-
if (rule.meta.docs.recommended === 'recommended') {
62+
expect(ruleRec).toMatch(/^(recommended|strict)$/);
63+
if (ruleRec === 'recommended') {
6264
expect(plugin.configs.recommended.rules).toHaveProperty(fullRuleName);
6365
} else {
6466
expect(plugin.configs.recommended.rules).not.toHaveProperty(fullRuleName);
@@ -67,14 +69,17 @@ describe('package', () => {
6769
// Strict configuration always includes all recommended rules.
6870
// Not allowed to specify non-default options since rule only specifies a configuration name.
6971
expect(plugin.configs.strict.rules).toHaveProperty(fullRuleName, expect.any(String));
72+
} else if (typeof ruleRec !== 'object' || !('strict' in ruleRec && Array.isArray(ruleRec.strict))) {
73+
// Rule has invalid recommended configuration.
74+
expect.fail(`Unexpected type for 'rule.meta.docs.recommended': '${typeof ruleRec}'.`);
7075
} else {
7176
// Rule specifies non-default options for strict.
72-
if (rule.meta.docs.recommended.recommended) {
77+
if ('recommended' in ruleRec) {
7378
expect(plugin.configs.recommended.rules).toHaveProperty(fullRuleName);
7479
} else {
7580
expect(plugin.configs.recommended.rules).not.toHaveProperty(fullRuleName);
7681
}
77-
expect(plugin.configs.strict.rules).toHaveProperty(fullRuleName, [expect.any(String), rule.meta.docs.recommended.strict[0]]);
82+
expect(plugin.configs.strict.rules).toHaveProperty(fullRuleName, [expect.any(String), ruleRec.strict[0]]);
7883
}
7984
});
8085
});

0 commit comments

Comments
 (0)