Skip to content

Commit dc37d9d

Browse files
committed
fix(eslint-plugin-qwik): fix ESLint 9 compatibility, improve typing and docs
1 parent 133a7cb commit dc37d9d

File tree

4 files changed

+155
-35
lines changed

4 files changed

+155
-35
lines changed

.changeset/giant-lobsters-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-qwik': patch
3+
---
4+
5+
Fix ESLint 9 compatibility, enhance typing and README.

packages/eslint-plugin-qwik/README.md

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,109 @@
11
# eslint-plugin-qwik
22

3-
## Implemented
3+
Qwik comes with its own set of ESLint rules to help developers write better code.
44

5-
- `no-use-after-await` (deprecated)
6-
- `valid-lexical-scope`
5+
## Usage
6+
7+
Install the plugin:
8+
9+
```bash
10+
npm add -D eslint-plugin-qwik
11+
pnpm add -D eslint-plugin-qwik
12+
yarn add -D eslint-plugin-qwik
13+
```
14+
15+
> `eslint-plugin-qwik` uses the tsc typechecker to type information. You must include the `tsconfigRootDir` in the `parserOptions`.
16+
17+
## Configurations
18+
19+
### Flat config (recommended)
20+
21+
```js
22+
// eslint.config.js
23+
import js from '@eslint/js';
24+
import globals from 'globals';
25+
import tseslint from 'typescript-eslint';
26+
import { qwikEslint9Plugin } from 'eslint-plugin-qwik';
27+
28+
export const qwikConfig = [
29+
js.configs.recommended,
30+
...tseslint.configs.recommended,
31+
{
32+
languageOptions: {
33+
parserOptions: {
34+
projectService: true,
35+
tsconfigRootDir: import.meta.dirname,
36+
},
37+
},
38+
},
39+
{
40+
files: ['**/*.{js,mjs,cjs,jsx,mjsx,ts,tsx,mtsx}'],
41+
languageOptions: {
42+
globals: {
43+
...globals.serviceworker,
44+
...globals.browser,
45+
...globals.node,
46+
},
47+
},
48+
},
49+
...qwikEslint9Plugin.configs.recommended,
50+
{
51+
ignores: ['node_modules/*', 'dist/*', 'server/*', 'tmp/*'],
52+
},
53+
];
54+
```
55+
56+
> `ignores` must always be the last configuration in the array. Note that in shared configs, the `ignores` must be defined where the config is used, not in the shared config.
57+
58+
### Legacy config (`eslint < 9`)
59+
60+
```js
61+
// .eslintrc.js
62+
module.exports = {
63+
env: {
64+
browser: true,
65+
es2021: true,
66+
node: true,
67+
},
68+
extends: [
69+
'eslint:recommended',
70+
'plugin:@typescript-eslint/recommended',
71+
'plugin:qwik/recommended',
72+
],
73+
parser: '@typescript-eslint/parser',
74+
parserOptions: {
75+
tsconfigRootDir: __dirname,
76+
project: ['./tsconfig.json'],
77+
ecmaVersion: 2021,
78+
sourceType: 'module',
79+
ecmaFeatures: {
80+
jsx: true,
81+
},
82+
},
83+
plugins: ['@typescript-eslint'],
84+
};
85+
```
86+
87+
> To ignore files, you must use the `.eslintignore` file.
88+
89+
## List of supported rules
90+
91+
- **Warn** in 'recommended' ruleset — ✔️
92+
- **Error** in 'recommended' ruleset — ✅
93+
- **Warn** in 'strict' ruleset — 🔒
94+
- **Error** in 'strict' ruleset — 🔐
95+
- **Typecheck** — 💭
96+
97+
| Rule | Description | Ruleset |
98+
| ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
99+
| [`qwik/valid-lexical-scope`](https://qwik.dev/docs/advanced/eslint/#valid-lexical-scope) | Used the tsc typechecker to detect the capture of unserializable data in dollar `($)` scopes. | ✅ 🔐 💭 |
100+
| [`qwik/use-method-usage`](https://qwik.dev/docs/advanced/eslint/#use-method-usage) | Detect invalid use of use hook. | ✅ 🔐 |
101+
| [`qwik/no-react-props`](https://qwik.dev/docs/advanced/eslint/#no-react-props) | Disallow usage of React-specific `className/htmlFor` props. | ✅ 🔐 |
102+
| [`qwik/loader-location`](https://qwik.dev/docs/advanced/eslint/#loader-location) | Detect declaration location of `loader$`. | ✔️ 🔐 |
103+
| [`qwik/prefer-classlist`](https://qwik.dev/docs/advanced/eslint/#prefer-classlist) | Enforce using the `classlist` prop over importing a `classnames` helper. The `classlist` prop accepts an object `{ [class: string]: boolean }` just like `classnames`. | ✔️ 🔐 |
104+
| [`qwik/jsx-no-script-url`](https://qwik.dev/docs/advanced/eslint/#jsx-no-script-url) | Disallow javascript: URLs. | ✔️ 🔐 |
105+
| [`qwik/jsx-key`](https://qwik.dev/docs/advanced/eslint/#jsx-key) | Disallow missing `key` props in iterators/collection literals. | ✔️ 🔐 |
106+
| [`qwik/unused-server`](https://qwik.dev/docs/advanced/eslint/#unused-server) | Detect unused `server$()` functions. | ✅ 🔐 |
107+
| [`qwik/jsx-img`](https://qwik.dev/docs/advanced/eslint/#jsx-img) | For performance reasons, always provide width and height attributes for `<img>` elements, it will help to prevent layout shifts. | ✔️ 🔐 |
108+
| [`qwik/jsx-a`](https://qwik.dev/docs/advanced/eslint/#jsx-a) | For a perfect SEO score, always provide href attribute for `<a>` elements. | ✔️ 🔐 |
109+
| [`qwik/no-use-visible-task`](https://qwik.dev/docs/advanced/eslint/#no-use-visible-task) | Detect `useVisibleTask$()` functions. | ✔️ 🔒 |

packages/eslint-plugin-qwik/index.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { TSESLint } from '@typescript-eslint/utils';
12
import { jsxAtag } from './src/jsxAtag';
23
import { jsxImg } from './src/jsxImg';
34
import { jsxKey } from './src/jsxKey';
@@ -11,76 +12,69 @@ import { useMethodUsage } from './src/useMethodUsage';
1112
import { validLexicalScope } from './src/validLexicalScope';
1213
import pkg from './package.json';
1314

15+
type Rules = NonNullable<TSESLint.FlatConfig.Plugin['rules']>;
16+
1417
const rules = {
15-
'use-method-usage': useMethodUsage,
1618
'valid-lexical-scope': validLexicalScope,
17-
'loader-location': loaderLocation,
19+
'use-method-usage': useMethodUsage,
1820
'no-react-props': noReactProps,
21+
'loader-location': loaderLocation,
1922
'prefer-classlist': preferClasslist,
2023
'jsx-no-script-url': jsxNoScriptUrl,
2124
'jsx-key': jsxKey,
2225
'unused-server': unusedServer,
2326
'jsx-img': jsxImg,
2427
'jsx-a': jsxAtag,
2528
'no-use-visible-task': noUseVisibleTask,
26-
};
29+
} as const satisfies Rules;
2730

28-
const recommendedRules = {
29-
'qwik/use-method-usage': 'error',
31+
const recommendedRulesLevels = {
3032
'qwik/valid-lexical-scope': 'error',
33+
'qwik/use-method-usage': 'error',
3134
'qwik/no-react-props': 'error',
35+
'qwik/loader-location': 'warn',
3236
'qwik/prefer-classlist': 'warn',
3337
'qwik/jsx-no-script-url': 'warn',
34-
'qwik/loader-location': 'warn',
3538
'qwik/jsx-key': 'warn',
3639
'qwik/unused-server': 'error',
3740
'qwik/jsx-img': 'warn',
3841
'qwik/jsx-a': 'warn',
3942
'qwik/no-use-visible-task': 'warn',
40-
};
41-
const strictRules = {
43+
} as const satisfies TSESLint.FlatConfig.Rules;
44+
45+
const strictRulesLevels = {
4246
'qwik/valid-lexical-scope': 'error',
4347
'qwik/use-method-usage': 'error',
44-
'qwik/loader-location': 'error',
4548
'qwik/no-react-props': 'error',
49+
'qwik/loader-location': 'error',
4650
'qwik/prefer-classlist': 'error',
4751
'qwik/jsx-no-script-url': 'error',
4852
'qwik/jsx-key': 'error',
4953
'qwik/unused-server': 'error',
5054
'qwik/jsx-img': 'error',
5155
'qwik/jsx-a': 'error',
5256
'qwik/no-use-visible-task': 'warn',
53-
};
57+
} as const satisfies TSESLint.FlatConfig.Rules;
5458

5559
const configs = {
5660
recommended: {
5761
plugins: ['qwik'],
58-
rules: recommendedRules,
62+
rules: recommendedRulesLevels,
5963
},
6064
strict: {
6165
plugins: ['qwik'],
62-
rules: strictRules,
66+
rules: strictRulesLevels,
6367
},
64-
};
68+
} as const satisfies Record<string, TSESLint.ClassicConfig.Config>;
6569

66-
const qwikEslint9Plugin = {
70+
const qwikEslint9Plugin: TSESLint.FlatConfig.Plugin = {
6771
configs: {
68-
recommended: [
69-
{
70-
plugins: {
71-
qwik: this,
72-
},
73-
rules: recommendedRules,
74-
},
75-
],
76-
strict: [
77-
{
78-
plugins: {
79-
qwik: this,
80-
},
81-
rules: strictRules,
82-
},
83-
],
72+
get recommended() {
73+
return recommendedConfig;
74+
},
75+
get strict() {
76+
return strictConfig;
77+
},
8478
},
8579
meta: {
8680
name: pkg.name,
@@ -89,4 +83,22 @@ const qwikEslint9Plugin = {
8983
rules,
9084
};
9185

86+
const recommendedConfig: TSESLint.FlatConfig.ConfigArray = [
87+
{
88+
plugins: {
89+
qwik: qwikEslint9Plugin,
90+
},
91+
rules: recommendedRulesLevels,
92+
},
93+
];
94+
95+
const strictConfig: TSESLint.FlatConfig.ConfigArray = [
96+
{
97+
plugins: {
98+
qwik: qwikEslint9Plugin,
99+
},
100+
rules: strictRulesLevels,
101+
},
102+
];
103+
92104
export { configs, qwikEslint9Plugin, rules };

packages/eslint-plugin-qwik/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"devDependencies": {
1212
"@builder.io/qwik": "workspace:^",
1313
"@builder.io/qwik-city": "workspace:^",
14+
"@types/eslint": "9.6.1",
1415
"@types/estree": "1.0.5",
1516
"@typescript-eslint/rule-tester": "8.14.0",
16-
"redent": "4.0.0",
17-
"@types/eslint": "9.6.1"
17+
"redent": "4.0.0"
1818
},
1919
"engines": {
2020
"node": ">=16.8.0 <18.0.0 || >=18.11"

0 commit comments

Comments
 (0)