@hypersolid-dev/eslint-config 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +149 -2
- package/dist/base.d.ts +6 -0
- package/dist/base.js +121 -0
- package/dist/next.d.ts +2 -0
- package/dist/next.js +16 -0
- package/dist/node.d.ts +2 -0
- package/dist/node.js +12 -0
- package/dist/react-native.d.ts +2 -0
- package/dist/react-native.js +32 -0
- package/dist/react.d.ts +2 -0
- package/dist/react.js +17 -0
- package/dist/shared.d.ts +9 -0
- package/dist/shared.js +58 -0
- package/package.json +75 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hypersolid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @hypersolid-dev/eslint-config
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared ESLint flat config for Hypersolid TypeScript projects. Type-aware by default, every enabled rule is an `error`, and formatting is left to your formatter.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node 24+
|
|
8
|
+
- ESLint `^10.4.0` (flat config only, no `.eslintrc`)
|
|
9
|
+
- TypeScript `>=5.8 <6.1` (typescript-eslint does not support TypeScript 7 yet)
|
|
10
|
+
- A `tsconfig.json` that includes the files you lint
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm add -D @hypersolid-dev/eslint-config eslint typescript
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The plugins are regular dependencies of this package, so you don't install them yourself.
|
|
19
|
+
|
|
20
|
+
## Presets
|
|
21
|
+
|
|
22
|
+
| Preset | Import | For |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `base` | `@hypersolid-dev/eslint-config/base` | Every TypeScript project. Required, and always first. |
|
|
25
|
+
| `react` | `@hypersolid-dev/eslint-config/react` | React for the web. |
|
|
26
|
+
| `react-native` | `@hypersolid-dev/eslint-config/react-native` | React Native and Expo. Use instead of `react`. |
|
|
27
|
+
| `node` | `@hypersolid-dev/eslint-config/node` | Node.js services, scripts and CLIs. |
|
|
28
|
+
| `next` | `@hypersolid-dev/eslint-config/next` | Next.js. Stack on top of `react`. |
|
|
29
|
+
|
|
30
|
+
Each preset is a function that returns an array of config objects. Presets don't include `base`: spread `base` first, then the presets you need, then your own overrides.
|
|
31
|
+
|
|
32
|
+
`base` takes `tsconfigRootDir`, the directory your `tsconfig.json` lives in, and throws without it. In an `eslint.config.js` next to your `tsconfig.json`, that's `import.meta.dirname`.
|
|
33
|
+
|
|
34
|
+
### TypeScript library or package
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// eslint.config.js
|
|
38
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
39
|
+
|
|
40
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname })];
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Node.js
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
47
|
+
import node from '@hypersolid-dev/eslint-config/node';
|
|
48
|
+
|
|
49
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), ...node()];
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### React
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
56
|
+
import react from '@hypersolid-dev/eslint-config/react';
|
|
57
|
+
|
|
58
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react()];
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### React Native / Expo
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
65
|
+
import reactNative from '@hypersolid-dev/eslint-config/react-native';
|
|
66
|
+
|
|
67
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), ...reactNative()];
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`react-native` bans browser globals that don't exist on React Native (`document`, `localStorage`, …), and ignores `.expo`, `android`, `ios` and `expo-env.d.ts`.
|
|
71
|
+
|
|
72
|
+
### Next.js
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
76
|
+
import next from '@hypersolid-dev/eslint-config/next';
|
|
77
|
+
import react from '@hypersolid-dev/eslint-config/react';
|
|
78
|
+
|
|
79
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react(), ...next()];
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Overriding rules
|
|
83
|
+
|
|
84
|
+
Add your own config objects after the presets. A later object wins for the files it matches.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
import base from '@hypersolid-dev/eslint-config/base';
|
|
88
|
+
|
|
89
|
+
export default [
|
|
90
|
+
...base({ tsconfigRootDir: import.meta.dirname }),
|
|
91
|
+
{
|
|
92
|
+
rules: {
|
|
93
|
+
// Turn a rule off everywhere.
|
|
94
|
+
'unicorn/consistent-boolean-name': 'off',
|
|
95
|
+
// Change a rule's options; restate the severity.
|
|
96
|
+
curly: ['error', 'all'],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
// Relax rules for one part of the project.
|
|
101
|
+
files: ['scripts/**'],
|
|
102
|
+
rules: {
|
|
103
|
+
'no-console': 'off',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A rule's options are replaced, not merged. For example, overriding `no-restricted-globals` drops the presets' entries (`isNaN`, `isFinite`, and the browser globals in `react-native`), so repeat any you want to keep.
|
|
110
|
+
|
|
111
|
+
To ignore files, add an object with only `ignores`:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['generated/**'] }];
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`base` already ignores `node_modules`, `dist`, `build`, `coverage` and lockfiles.
|
|
118
|
+
|
|
119
|
+
## Formatting and import order
|
|
120
|
+
|
|
121
|
+
This config has no formatting or stylistic rules, and it doesn't sort imports (`import-x/order` is off). Use a formatter such as Prettier or Biome for both.
|
|
122
|
+
|
|
123
|
+
## Root config files
|
|
124
|
+
|
|
125
|
+
Root files such as `eslint.config.js` or `vite.config.ts` are often outside the tsconfig's `include`. `base` lints `*.config.{js,mjs,cjs,ts,mts,cts}` in the project root with a default project, so they don't need to be in your tsconfig.
|
|
126
|
+
|
|
127
|
+
## Known issues
|
|
128
|
+
|
|
129
|
+
- `eslint-plugin-jsx-a11y` (used by `react`) declares support for ESLint 9 only. It works on ESLint 10, but installing prints a peer-dependency warning, and installs with `strict-peer-dependencies` fail.
|
|
130
|
+
- In `next`, `@next/next/no-html-link-for-pages` prints "Pages directory cannot be found" on App Router projects. Turn it off if you don't use the `pages` directory:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
{ rules: { '@next/next/no-html-link-for-pages': 'off' } }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
pnpm install --ignore-scripts
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pnpm lint
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pnpm test
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`pnpm lint` builds the package and lints this repo with its own `base` preset. `pnpm test` runs fixture tests: for each preset, every invalid fixture in `test/fixtures/<preset>/invalid` must trigger the rule it's named after, and the valid fixtures must produce no messages. See [AGENTS.md](AGENTS.md) for conventions.
|
package/dist/base.d.ts
ADDED
package/dist/base.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import json from '@eslint/json';
|
|
3
|
+
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript';
|
|
4
|
+
import { createNodeResolver, flatConfigs as importX } from 'eslint-plugin-import-x';
|
|
5
|
+
import noSecrets from 'eslint-plugin-no-secrets';
|
|
6
|
+
import unicorn from 'eslint-plugin-unicorn';
|
|
7
|
+
import { configs as tsConfigs } from 'typescript-eslint';
|
|
8
|
+
import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode } from './shared.js';
|
|
9
|
+
export default function base(options) {
|
|
10
|
+
if (!options.tsconfigRootDir) {
|
|
11
|
+
throw new Error('@hypersolid-dev/eslint-config: base() requires tsconfigRootDir');
|
|
12
|
+
}
|
|
13
|
+
return raiseWarnings([
|
|
14
|
+
{
|
|
15
|
+
name: '@hypersolid-dev/base/ignores',
|
|
16
|
+
ignores: ['**/node_modules', '**/dist', '**/build', '**/coverage', '**/pnpm-lock.yaml', '**/package-lock.json', '**/yarn.lock'],
|
|
17
|
+
},
|
|
18
|
+
...scopeToCode([
|
|
19
|
+
// ESLint's own bug-catching rules. Bad: `if (x = 1)`, `const a = 1; const a = 2;`.
|
|
20
|
+
js.configs.recommended,
|
|
21
|
+
// Type-aware TypeScript rules, the strict set. Bad: `const n: any = data; n.foo()`, `if (alwaysTrue)`.
|
|
22
|
+
...tsConfigs.strictTypeChecked,
|
|
23
|
+
// Unicorn's recommended set minus its taste rules (file naming, `no-null`, `no-array-reduce`,
|
|
24
|
+
// abbreviations, formatting); what remains catches bugs and outdated APIs.
|
|
25
|
+
// Bad: `array.indexOf(x) !== -1` (use `includes`), `new Error()` without a message.
|
|
26
|
+
unicorn.configs.unopinionated,
|
|
27
|
+
// Import mistakes. Bad: `import { missing } from './module.js'`, importing the same module twice.
|
|
28
|
+
importX.recommended,
|
|
29
|
+
// Makes import-x understand TypeScript files and `import type`.
|
|
30
|
+
importX.typescript,
|
|
31
|
+
]),
|
|
32
|
+
{
|
|
33
|
+
name: '@hypersolid-dev/base',
|
|
34
|
+
files: CODE_FILES,
|
|
35
|
+
languageOptions: {
|
|
36
|
+
parserOptions: {
|
|
37
|
+
// Root config files usually sit outside the tsconfig's `include`.
|
|
38
|
+
projectService: { allowDefaultProject: ['*.config.{js,mjs,cjs,ts,mts,cts}'] },
|
|
39
|
+
tsconfigRootDir: options.tsconfigRootDir,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
plugins: { 'no-secrets': noSecrets },
|
|
43
|
+
settings: {
|
|
44
|
+
'import-x/resolver-next': [createTypeScriptImportResolver(), createNodeResolver()],
|
|
45
|
+
},
|
|
46
|
+
rules: {
|
|
47
|
+
// Braces on any multi-line body. Bad: `if (a)\n return 1;`.
|
|
48
|
+
curly: ['error', 'multi-line'],
|
|
49
|
+
// Strict equality only, `null` included. Bad: `a == b`.
|
|
50
|
+
eqeqeq: ['error', 'always'],
|
|
51
|
+
// No leftover debug output. Bad: `console.log(user)`.
|
|
52
|
+
'no-console': 'error',
|
|
53
|
+
// One ternary per expression. Bad: `a ? 'x' : b ? 'y' : 'z'`.
|
|
54
|
+
'no-nested-ternary': 'error',
|
|
55
|
+
// Arrow functions as callbacks. Bad: `list.map(function (x) { return x; })`.
|
|
56
|
+
'prefer-arrow-callback': 'error',
|
|
57
|
+
// A rethrow keeps the original error as `cause`. Bad: `catch (error) { throw new Error('failed'); }`.
|
|
58
|
+
'preserve-caught-error': 'error',
|
|
59
|
+
// The coercing globals. Bad: `isNaN('x')` (use `Number.isNaN`).
|
|
60
|
+
'no-restricted-globals': ['error', ...NUMBER_GLOBAL_RESTRICTIONS],
|
|
61
|
+
// `type` over `interface`. Bad: `interface User { name: string }`.
|
|
62
|
+
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
|
63
|
+
// Every promise is awaited or handled; `void` does not count. Bad: `save();`, `void save();`.
|
|
64
|
+
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: false }],
|
|
65
|
+
// Type-only imports use `import type`, so nothing is loaded at runtime. Bad: `import { type User } from './user.js'`.
|
|
66
|
+
'@typescript-eslint/no-import-type-side-effects': 'error',
|
|
67
|
+
// `??` over `||` for defaults, so `0` and `''` survive. Bad: `count || 10`.
|
|
68
|
+
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
|
69
|
+
// `sort()` needs a comparator; the default sorts numbers as strings. Bad: `[10, 9].sort()`.
|
|
70
|
+
'@typescript-eslint/require-array-sort-compare': 'error',
|
|
71
|
+
// No value returned where `void` is expected. Bad: `run(() => 1)` with `run(callback: () => void)`.
|
|
72
|
+
'@typescript-eslint/strict-void-return': ['error', { allowReturnAny: true }],
|
|
73
|
+
// A `switch` on a union handles every member. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
|
|
74
|
+
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
75
|
+
// The caught value is named `error`. Bad: `catch (e)`.
|
|
76
|
+
'unicorn/catch-error-name': 'error',
|
|
77
|
+
// A boolean reads as a claim. Bad: `const visible = true` (use `isVisible`).
|
|
78
|
+
'unicorn/consistent-boolean-name': ['error', { checkFunctions: 'never' }],
|
|
79
|
+
// A compound word is one word. Bad: `callBack`, `dataBase`.
|
|
80
|
+
'unicorn/consistent-compound-words': 'error',
|
|
81
|
+
// No non-standard abbreviations. Bad: `btn`, `cfg`. Allowed: `props`, `args`, `fn`, `ref`, `env`, `params`.
|
|
82
|
+
'unicorn/name-replacements': [
|
|
83
|
+
'error',
|
|
84
|
+
{ replacements: { props: false, args: false, fn: false, ref: false, env: false, params: false } },
|
|
85
|
+
],
|
|
86
|
+
// A verb prefix promises a function. Bad: `const getUser = { name: 'Ada' }`.
|
|
87
|
+
'unicorn/no-non-function-verb-prefix': 'error',
|
|
88
|
+
// `finally` does not wait for an async callback. Bad: `promise.finally(async () => { await close(); })`.
|
|
89
|
+
'unicorn/no-async-promise-finally': 'error',
|
|
90
|
+
// `await` inside `Promise.all` runs the calls one by one. Bad: `Promise.all([await a, await b])`.
|
|
91
|
+
'unicorn/no-await-in-promise-methods': 'error',
|
|
92
|
+
// Only the first resolve/reject counts. Bad: `new Promise((resolve) => { resolve(1); resolve(2); })`.
|
|
93
|
+
'unicorn/no-multiple-promise-resolver-calls': 'error',
|
|
94
|
+
// `Promise.all` around one promise. Bad: `Promise.all([fetchUser()])`.
|
|
95
|
+
'unicorn/no-single-promise-in-promise-methods': 'error',
|
|
96
|
+
// A `then` property makes `await` treat the object as a promise. Bad: `const task = { then() {} }`.
|
|
97
|
+
'unicorn/no-thenable': 'error',
|
|
98
|
+
// `.value` exists only on fulfilled results. Bad: `(await Promise.allSettled(list)).map((result) => result.value)`.
|
|
99
|
+
'unicorn/no-unsafe-promise-all-settled-values': 'error',
|
|
100
|
+
// An async function already wraps its return value. Bad: `async () => Promise.resolve(1)`.
|
|
101
|
+
'unicorn/no-useless-promise-resolve-reject': 'error',
|
|
102
|
+
// Import order is formatting, out of scope (AGENTS.md).
|
|
103
|
+
'import-x/order': 'off',
|
|
104
|
+
// High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
|
|
105
|
+
'no-secrets/no-secrets': 'error',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: '@hypersolid-dev/base/json',
|
|
110
|
+
files: ['**/*.json'],
|
|
111
|
+
language: 'json/json',
|
|
112
|
+
plugins: { json, 'no-secrets': noSecrets },
|
|
113
|
+
rules: {
|
|
114
|
+
// JSON correctness. Bad: `{ "name": "a", "name": "b" }`.
|
|
115
|
+
...json.configs.recommended.rules,
|
|
116
|
+
// Letters-only strings are keys and words (`verbatimModuleSyntax`), not credentials.
|
|
117
|
+
'no-secrets/no-secrets': ['error', { ignoreContent: ['^[A-Za-z]+$'] }],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
}
|
package/dist/next.d.ts
ADDED
package/dist/next.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import nextPlugin from '@next/eslint-plugin-next';
|
|
2
|
+
import { raiseWarnings, scopeToCode } from './shared.js';
|
|
3
|
+
// eslint-disable-next-line import-x/no-named-as-default-member -- CommonJS: its types list a named `configs` export that Node cannot import from ESM.
|
|
4
|
+
const { configs: nextConfigs } = nextPlugin;
|
|
5
|
+
export default function next() {
|
|
6
|
+
return raiseWarnings([
|
|
7
|
+
{
|
|
8
|
+
name: '@hypersolid-dev/next/ignores',
|
|
9
|
+
ignores: ['**/.next', '**/out'],
|
|
10
|
+
},
|
|
11
|
+
...scopeToCode([
|
|
12
|
+
// Next.js rules, `recommended` plus the Core Web Vitals ones. Bad: `<img src={src} />` (use `next/image`), `<script src={url} />` without `async`.
|
|
13
|
+
nextConfigs['core-web-vitals'],
|
|
14
|
+
]),
|
|
15
|
+
]);
|
|
16
|
+
}
|
package/dist/node.d.ts
ADDED
package/dist/node.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import globals from 'globals';
|
|
2
|
+
import { CODE_FILES } from './shared.js';
|
|
3
|
+
export default function node() {
|
|
4
|
+
return [
|
|
5
|
+
{
|
|
6
|
+
name: '@hypersolid-dev/node/globals',
|
|
7
|
+
files: CODE_FILES,
|
|
8
|
+
// Node's globals (`process`, `Buffer`, `__dirname`) for `no-undef` in JavaScript files; TypeScript files get them from `@types/node`.
|
|
9
|
+
languageOptions: { globals: globals.node },
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import globals from 'globals';
|
|
2
|
+
import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, reactConfigs } from './shared.js';
|
|
3
|
+
// Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
|
|
4
|
+
// `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
|
|
5
|
+
// so new browser APIs are picked up without editing this file.
|
|
6
|
+
// Globals Hermes provides that `globals['react-native']` does not list yet; remove a name once `globals` adds it.
|
|
7
|
+
const HERMES_GLOBALS = new Set(['atob', 'btoa', 'TextEncoder']);
|
|
8
|
+
const BROWSER_GLOBALS = Object.keys(globals.browser).filter((name) => !(name in globals['react-native']) && !HERMES_GLOBALS.has(name));
|
|
9
|
+
export default function reactNative() {
|
|
10
|
+
return raiseWarnings([
|
|
11
|
+
{
|
|
12
|
+
name: '@hypersolid-dev/react-native/ignores',
|
|
13
|
+
ignores: ['**/.expo', '**/android', '**/ios', '**/expo-env.d.ts'],
|
|
14
|
+
},
|
|
15
|
+
...reactConfigs(),
|
|
16
|
+
{
|
|
17
|
+
name: '@hypersolid-dev/react-native',
|
|
18
|
+
files: CODE_FILES,
|
|
19
|
+
rules: {
|
|
20
|
+
// Browser globals React Native lacks, and the coercing number globals. Bad: `document.title`, `isNaN(value)`.
|
|
21
|
+
'no-restricted-globals': [
|
|
22
|
+
'error',
|
|
23
|
+
...NUMBER_GLOBAL_RESTRICTIONS,
|
|
24
|
+
...BROWSER_GLOBALS.map((name) => ({
|
|
25
|
+
name,
|
|
26
|
+
message: `\`${name}\` is a browser global, undefined on React Native: a local of that name was meant, or the value comes from a React Native API.`,
|
|
27
|
+
})),
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
package/dist/react.d.ts
ADDED
package/dist/react.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
|
2
|
+
import globals from 'globals';
|
|
3
|
+
import { CODE_FILES, raiseWarnings, reactConfigs, scopeToCode } from './shared.js';
|
|
4
|
+
export default function react() {
|
|
5
|
+
return raiseWarnings([
|
|
6
|
+
...reactConfigs(),
|
|
7
|
+
...scopeToCode([
|
|
8
|
+
// Accessible markup. Bad: `<img src={src} />` without `alt`, `<div onClick={open}>` without a role.
|
|
9
|
+
jsxA11y.flatConfigs.recommended,
|
|
10
|
+
]),
|
|
11
|
+
{
|
|
12
|
+
name: '@hypersolid-dev/react/globals',
|
|
13
|
+
files: CODE_FILES,
|
|
14
|
+
languageOptions: { globals: globals.browser },
|
|
15
|
+
},
|
|
16
|
+
]);
|
|
17
|
+
}
|
package/dist/shared.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Linter } from 'eslint';
|
|
2
|
+
export declare const CODE_FILES: string[];
|
|
3
|
+
export declare const NUMBER_GLOBAL_RESTRICTIONS: {
|
|
4
|
+
name: string;
|
|
5
|
+
message: string;
|
|
6
|
+
}[];
|
|
7
|
+
export declare function raiseWarnings(configs: Linter.Config[]): Linter.Config[];
|
|
8
|
+
export declare function scopeToCode(configs: readonly Linter.Config[]): Linter.Config[];
|
|
9
|
+
export declare function reactConfigs(): Linter.Config[];
|
package/dist/shared.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import eslintReact from '@eslint-react/eslint-plugin';
|
|
2
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
3
|
+
export const CODE_FILES = ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
|
|
4
|
+
// Shared by every preset that sets `no-restricted-globals`: a later config replaces the rule's options, not merges them.
|
|
5
|
+
export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
|
|
6
|
+
name,
|
|
7
|
+
message: `Use Number.${name}; the global coerces its argument.`,
|
|
8
|
+
}));
|
|
9
|
+
// Every enabled rule is an error: a warning is either worth fixing or not worth reporting.
|
|
10
|
+
export function raiseWarnings(configs) {
|
|
11
|
+
return configs.map((config) => {
|
|
12
|
+
if (!config.rules) {
|
|
13
|
+
return config;
|
|
14
|
+
}
|
|
15
|
+
const rules = {};
|
|
16
|
+
for (const [name, entry] of Object.entries(config.rules)) {
|
|
17
|
+
if (entry === undefined) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const [severity, ...ruleOptions] = Array.isArray(entry) ? entry : [entry];
|
|
21
|
+
rules[name] = severity === 'warn' || severity === 1 ? ['error', ...ruleOptions] : entry;
|
|
22
|
+
}
|
|
23
|
+
return { ...config, rules };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function scopeToCode(configs) {
|
|
27
|
+
return configs.map((config) => ({ files: CODE_FILES, ...config }));
|
|
28
|
+
}
|
|
29
|
+
// The React block shared by `react` and `react-native`.
|
|
30
|
+
export function reactConfigs() {
|
|
31
|
+
return [
|
|
32
|
+
...scopeToCode([
|
|
33
|
+
// React correctness for TypeScript projects. Bad: `this.state.count = 1`, `<li>` in a `.map()` without `key`.
|
|
34
|
+
eslintReact.configs['recommended-typescript'],
|
|
35
|
+
// Turns off @eslint-react's own hooks rules so react-hooks (below) is the one source for them.
|
|
36
|
+
eslintReact.configs['disable-conflict-eslint-plugin-react-hooks'],
|
|
37
|
+
// The official hooks rules plus the React Compiler checks. Bad: `if (isOpen) useEffect(…)`, a missing effect dependency.
|
|
38
|
+
reactHooks.configs.flat['recommended-latest'],
|
|
39
|
+
]),
|
|
40
|
+
{
|
|
41
|
+
name: '@hypersolid-dev/react',
|
|
42
|
+
files: CODE_FILES,
|
|
43
|
+
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
|
44
|
+
rules: {
|
|
45
|
+
// A component defined inside another remounts on every render. Bad: `function List() { function Row() {…} return <Row />; }`.
|
|
46
|
+
'@eslint-react/no-nested-component-definitions': 'error',
|
|
47
|
+
// List items need a `key`. Bad: `items.map((item) => <Row item={item} />)`.
|
|
48
|
+
'@eslint-react/no-missing-key': 'error',
|
|
49
|
+
// Sibling keys are unique. Bad: `<Row key="a" /><Row key="a" />`.
|
|
50
|
+
'@eslint-react/no-duplicate-key': 'error',
|
|
51
|
+
// An index key breaks state when the list reorders. Bad: `items.map((item, index) => <Row key={index} />)`.
|
|
52
|
+
'@eslint-react/no-array-index-key': 'error',
|
|
53
|
+
// A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
|
|
54
|
+
'@eslint-react/no-unstable-context-value': 'error',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypersolid-dev/eslint-config",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared ESLint flat config for Hypersolid TypeScript projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"eslint",
|
|
8
|
+
"eslint-config",
|
|
9
|
+
"eslintconfig",
|
|
10
|
+
"flat-config",
|
|
11
|
+
"typescript",
|
|
12
|
+
"react",
|
|
13
|
+
"react-native",
|
|
14
|
+
"next",
|
|
15
|
+
"node"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"exports": {
|
|
19
|
+
"./base": {
|
|
20
|
+
"types": "./dist/base.d.ts",
|
|
21
|
+
"default": "./dist/base.js"
|
|
22
|
+
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"types": "./dist/react.d.ts",
|
|
25
|
+
"default": "./dist/react.js"
|
|
26
|
+
},
|
|
27
|
+
"./react-native": {
|
|
28
|
+
"types": "./dist/react-native.d.ts",
|
|
29
|
+
"default": "./dist/react-native.js"
|
|
30
|
+
},
|
|
31
|
+
"./node": {
|
|
32
|
+
"types": "./dist/node.d.ts",
|
|
33
|
+
"default": "./dist/node.js"
|
|
34
|
+
},
|
|
35
|
+
"./next": {
|
|
36
|
+
"types": "./dist/next.d.ts",
|
|
37
|
+
"default": "./dist/next.js"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"eslint": "^10.4.0",
|
|
48
|
+
"typescript": ">=5.8 <6.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^24.19.2",
|
|
52
|
+
"@types/react": "^19.3.0",
|
|
53
|
+
"eslint": "^10.14.0",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@eslint-react/eslint-plugin": "^5.28.8",
|
|
58
|
+
"@eslint/js": "^10.0.1",
|
|
59
|
+
"@eslint/json": "^2.1.1",
|
|
60
|
+
"@next/eslint-plugin-next": "^16.4.3",
|
|
61
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
62
|
+
"eslint-plugin-import-x": "^4.17.1",
|
|
63
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
64
|
+
"eslint-plugin-no-secrets": "^2.3.3",
|
|
65
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
66
|
+
"eslint-plugin-unicorn": "^79.0.0",
|
|
67
|
+
"globals": "^17.13.0",
|
|
68
|
+
"typescript-eslint": "^8.72.1"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsc -p tsconfig.build.json",
|
|
75
|
+
"lint": "pnpm build && eslint .",
|
|
76
|
+
"test": "pnpm build && node --test 'test/*.test.ts'"
|
|
77
|
+
}
|
|
6
78
|
}
|