@hypersolid-dev/eslint-config 1.0.0 → 2.0.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/CHANGELOG.md +12 -0
- package/README.md +16 -0
- package/dist/base.d.ts +1 -0
- package/dist/base.js +63 -8
- package/dist/react-native.js +36 -1
- package/dist/shared.d.ts +8 -0
- package/dist/shared.js +22 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [2.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv1.0.0&targetVersion=GTv2.0.0) (2026-11-09)
|
|
2
|
+
|
|
3
|
+
### ⚠ BREAKING CHANGES
|
|
4
|
+
|
|
5
|
+
* **base:** switch-exhaustiveness-check now accepts a default branch as exhaustive.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **base:** retune rules after the ESPN Goal rule review ([113775e](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/113775ed1b1ae3abfcac948d55ba33328311b903)), closes [AB#5176](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5176)
|
|
10
|
+
* **react-native:** Reanimated boolean wrappers, Expo no-secrets ignores, browser globals in tests ([86bf9b3](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/86bf9b3149a0696b4cc9114ca7f60c5074c002c0)), closes [AB#5176](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5176)
|
|
11
|
+
* **react:** exempt hook and mock names from consistent-boolean-name, relax use-prefix in tests ([2d96538](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/2d96538f941a9b5cf28b28c9481875b3da2f6a67)), closes [AB#5176](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5176)
|
|
12
|
+
|
|
1
13
|
## [1.0.0](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv0.1.2&targetVersion=GTv1.0.0) (2026-11-06)
|
|
2
14
|
|
|
3
15
|
### ⚠ BREAKING CHANGES
|
package/README.md
CHANGED
|
@@ -124,6 +124,22 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['
|
|
|
124
124
|
|
|
125
125
|
`base` already ignores `node_modules`, `dist`, `build`, `coverage` and lockfiles.
|
|
126
126
|
|
|
127
|
+
### Test files
|
|
128
|
+
|
|
129
|
+
Every preset relaxes a few rules for tests, stories, mocks and test setup (`TEST_FILES`, exported from `@hypersolid-dev/eslint-config/base`): `@typescript-eslint/no-unsafe-assignment`, `no-unsafe-member-access`, `no-invalid-void-type`, `unicorn/no-error-property-assignment` and `no-global-object-property-assignment` in `base`; `@eslint-react/no-unnecessary-use-prefix` in `react` and `react-native`; and the browser-globals list (`no-restricted-globals`) in `react-native`. Source code keeps them all.
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
import base, { TEST_FILES } from '@hypersolid-dev/eslint-config/base';
|
|
133
|
+
|
|
134
|
+
export default [...base({ tsconfigRootDir: import.meta.dirname }), { files: TEST_FILES, rules: { 'no-console': 'off' } }];
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Preset-specific options
|
|
138
|
+
|
|
139
|
+
- `unicorn/consistent-boolean-name` skips function names in every preset; `react` and `react-native` also skip hook (`^use[A-Z]`) and jest mock (`^mock`) names, and `react-native` reads Reanimated's `SharedValue` through `.value`.
|
|
140
|
+
- `no-secrets/no-secrets` ignores `data:` URIs in JSON; in `react-native` it also ignores Expo and native identifiers (`EXPO_*`, `android.settings.*`, `@xml…`, `NSPrivacy*`, i18n `Namespace/Key` paths) in code and JSON.
|
|
141
|
+
- `@typescript-eslint/switch-exhaustiveness-check` accepts a `default` branch as exhaustive (`considerDefaultExhaustiveForUnions`). Set it to `false` to make every new union member fail its switches.
|
|
142
|
+
|
|
127
143
|
## Formatting and import order
|
|
128
144
|
|
|
129
145
|
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.
|
package/dist/base.d.ts
CHANGED
package/dist/base.js
CHANGED
|
@@ -5,7 +5,9 @@ import { createNodeResolver, flatConfigs as importX } from 'eslint-plugin-import
|
|
|
5
5
|
import noSecrets from 'eslint-plugin-no-secrets';
|
|
6
6
|
import unicorn from 'eslint-plugin-unicorn';
|
|
7
7
|
import { configs as tsConfigs } from 'typescript-eslint';
|
|
8
|
-
import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode } from './shared.js';
|
|
8
|
+
import { BOOLEAN_NAME_OPTIONS, CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, scopeToCode, TEST_FILES } from './shared.js';
|
|
9
|
+
// The glob the presets relax test-only rules for; reuse it for your own test overrides.
|
|
10
|
+
export { TEST_FILES } from './shared.js';
|
|
9
11
|
export default function base(options) {
|
|
10
12
|
if (!options.tsconfigRootDir) {
|
|
11
13
|
throw new Error('@hypersolid-dev/eslint-config: base() requires tsconfigRootDir');
|
|
@@ -66,16 +68,23 @@ export default function base(options) {
|
|
|
66
68
|
'@typescript-eslint/no-import-type-side-effects': 'error',
|
|
67
69
|
// `??` over `||` for defaults, so `0` and `''` survive. Bad: `count || 10`.
|
|
68
70
|
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
|
69
|
-
//
|
|
70
|
-
'@typescript-eslint/
|
|
71
|
+
// A void call as an expression; arrow shorthand like `onPress={() => setOpen(true)}` is fine. Bad: `const result = log(x)`.
|
|
72
|
+
'@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
|
|
73
|
+
// `sort()` needs a comparator, except on strings, where the default order is correct. Bad: `[10, 9].sort()`.
|
|
74
|
+
'@typescript-eslint/require-array-sort-compare': ['error', { ignoreStringArrays: true }],
|
|
75
|
+
// Only strings and numbers in template literals (strictTypeChecked's options plus numbers). Bad: `` `${user}` ``, `` `${null}` ``.
|
|
76
|
+
'@typescript-eslint/restrict-template-expressions': [
|
|
77
|
+
'error',
|
|
78
|
+
{ allowAny: false, allowBoolean: false, allowNever: false, allowNullish: false, allowNumber: true, allowRegExp: false },
|
|
79
|
+
],
|
|
71
80
|
// No value returned where `void` is expected. Bad: `run(() => 1)` with `run(callback: () => void)`.
|
|
72
81
|
'@typescript-eslint/strict-void-return': ['error', { allowReturnAny: true }],
|
|
73
|
-
// A `switch` on a union handles every member
|
|
74
|
-
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
|
82
|
+
// A `switch` on a union handles every member or has a `default`. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
|
|
83
|
+
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
|
75
84
|
// The caught value is named `error`. Bad: `catch (e)`.
|
|
76
85
|
'unicorn/catch-error-name': 'error',
|
|
77
86
|
// A boolean reads as a claim. Bad: `const visible = true` (use `isVisible`).
|
|
78
|
-
'unicorn/consistent-boolean-name': ['error',
|
|
87
|
+
'unicorn/consistent-boolean-name': ['error', BOOLEAN_NAME_OPTIONS],
|
|
79
88
|
// A compound word is one word. Bad: `callBack`, `dataBase`.
|
|
80
89
|
'unicorn/consistent-compound-words': 'error',
|
|
81
90
|
// No non-standard abbreviations. Bad: `btn`, `cfg`. Allowed: `props`, `args`, `fn`, `ref`, `env`, `params`.
|
|
@@ -99,12 +108,58 @@ export default function base(options) {
|
|
|
99
108
|
'unicorn/no-unsafe-promise-all-settled-values': 'error',
|
|
100
109
|
// An async function already wraps its return value. Bad: `async () => Promise.resolve(1)`.
|
|
101
110
|
'unicorn/no-useless-promise-resolve-reject': 'error',
|
|
111
|
+
// `undefined` as an argument is often required by the callee's signature. Bad: `let value = undefined`.
|
|
112
|
+
'unicorn/no-useless-undefined': ['error', { checkArguments: false }],
|
|
113
|
+
// Off: `.then()` chains are fine. Bad: `fetchUser().then(render)` reported.
|
|
114
|
+
'unicorn/prefer-await': 'off',
|
|
115
|
+
// Off: an `if`/`else` is often clearer than a ternary. Bad: `if (a) { x = 1 } else { x = 2 }` reported.
|
|
116
|
+
'unicorn/prefer-ternary': 'off',
|
|
117
|
+
// Off: a `case` next to `default` documents a known value. Bad: `case 'other': default:` reported.
|
|
118
|
+
'unicorn/no-useless-switch-case': 'off',
|
|
119
|
+
// Off: @typescript-eslint/require-array-sort-compare reports it with type information. Bad: `[10, 9].sort()` reported twice.
|
|
120
|
+
'unicorn/require-array-sort-compare': 'off',
|
|
121
|
+
// Off: DOM traversal style is taste. Bad: `element.parentNode` reported.
|
|
122
|
+
'unicorn/better-dom-traversing': 'off',
|
|
123
|
+
// Off: import style is taste. Bad: `import { join } from 'node:path'` reported.
|
|
124
|
+
'unicorn/import-style': 'off',
|
|
125
|
+
// Off: skipping elements by commas is fine. Bad: `const [, second] = list` reported.
|
|
126
|
+
'unicorn/no-unreadable-array-destructuring': 'off',
|
|
127
|
+
// Off: nested destructuring is fine. Bad: `const { a: { b } } = value` reported.
|
|
128
|
+
'unicorn/no-unreadable-object-destructuring': 'off',
|
|
129
|
+
// Off: `1.0` can document a float. Bad: `const opacity = 1.0` reported.
|
|
130
|
+
'unicorn/no-zero-fractions': 'off',
|
|
131
|
+
// Off: rarely relevant, noisy on intentional characters. Bad: a literal no-break space reported.
|
|
132
|
+
'unicorn/prefer-escaped-irregular-whitespace': 'off',
|
|
133
|
+
// Off: `window` and `global` are clearer where the platform is known. Bad: `window.location` reported.
|
|
134
|
+
'unicorn/prefer-global-this': 'off',
|
|
135
|
+
// Off: escaped strings are fine. Bad: `'C:\\temp'` reported.
|
|
136
|
+
'unicorn/prefer-string-raw': 'off',
|
|
137
|
+
// Off: rarely relevant. Bad: `Array(3).fill('-').join('')` reported.
|
|
138
|
+
'unicorn/prefer-string-repeat': 'off',
|
|
102
139
|
// Import order is formatting, out of scope (AGENTS.md).
|
|
103
140
|
'import-x/order': 'off',
|
|
141
|
+
// Off: TypeScript checks namespace members, and the rule is slow. Bad: `import * as math from './math.js'; math.missing` reported twice.
|
|
142
|
+
'import-x/namespace': 'off',
|
|
104
143
|
// High-entropy strings that look like credentials. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
|
|
105
144
|
'no-secrets/no-secrets': 'error',
|
|
106
145
|
},
|
|
107
146
|
},
|
|
147
|
+
{
|
|
148
|
+
name: '@hypersolid-dev/base/tests',
|
|
149
|
+
files: TEST_FILES,
|
|
150
|
+
rules: {
|
|
151
|
+
// Off: mocks and fixtures are loosely typed by design. Bad: `const user = jest.fn()()` reported.
|
|
152
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
153
|
+
// Off: mocks and fixtures are loosely typed by design. Bad: `mock.calls[0].user` reported.
|
|
154
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
155
|
+
// Off: mock signatures use `void` as a generic. Bad: `jest.fn<void, []>()` reported.
|
|
156
|
+
'@typescript-eslint/no-invalid-void-type': 'off',
|
|
157
|
+
// Off: tests build errors by hand. Bad: `error.code = 'ENOENT'` reported.
|
|
158
|
+
'unicorn/no-error-property-assignment': 'off',
|
|
159
|
+
// Off: test setup stubs globals. Bad: `globalThis.fetch = mockFetch` reported.
|
|
160
|
+
'unicorn/no-global-object-property-assignment': 'off',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
108
163
|
{
|
|
109
164
|
name: '@hypersolid-dev/base/json',
|
|
110
165
|
files: ['**/*.json'],
|
|
@@ -113,8 +168,8 @@ export default function base(options) {
|
|
|
113
168
|
rules: {
|
|
114
169
|
// JSON correctness. Bad: `{ "name": "a", "name": "b" }`.
|
|
115
170
|
...json.configs.recommended.rules,
|
|
116
|
-
// Letters-only strings are keys and words (`verbatimModuleSyntax`),
|
|
117
|
-
'no-secrets/no-secrets': ['error', { ignoreContent: ['^[A-Za-z]+$'] }],
|
|
171
|
+
// Letters-only strings are keys and words (`verbatimModuleSyntax`), and `data:` URIs are embedded images; neither is a credential.
|
|
172
|
+
'no-secrets/no-secrets': ['error', { ignoreContent: ['^[A-Za-z]+$', '^data:'] }],
|
|
118
173
|
},
|
|
119
174
|
},
|
|
120
175
|
]);
|
package/dist/react-native.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import globals from 'globals';
|
|
2
|
-
import
|
|
2
|
+
import noSecrets from 'eslint-plugin-no-secrets';
|
|
3
|
+
import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, REACT_BOOLEAN_NAME_OPTIONS, reactConfigs, TEST_FILES, } from './shared.js';
|
|
3
4
|
// Derived instead of hardcoded: the frozen `confusing-browser-globals` list (last released 2021) misses `document`,
|
|
4
5
|
// `localStorage` and `getComputedStyle`, and bans `self`, which React Native defines. `globals` is maintained,
|
|
5
6
|
// so new browser APIs are picked up without editing this file.
|
|
6
7
|
// Globals Hermes provides that `globals['react-native']` does not list yet; remove a name once `globals` adds it.
|
|
7
8
|
const HERMES_GLOBALS = new Set(['atob', 'btoa', 'TextEncoder']);
|
|
8
9
|
const BROWSER_GLOBALS = Object.keys(globals.browser).filter((name) => !(name in globals['react-native']) && !HERMES_GLOBALS.has(name));
|
|
10
|
+
// High-entropy strings in Expo and native config that are identifiers, not credentials.
|
|
11
|
+
const NO_SECRETS_IGNORE = [
|
|
12
|
+
'^android\\.settings\\.', // Android intent actions
|
|
13
|
+
'^EXPO_', // env var names
|
|
14
|
+
'^@xml', // Android resource references
|
|
15
|
+
'^NSPrivacy[A-Za-z]+$', // iOS privacy manifest keys
|
|
16
|
+
'^[A-Z][A-Za-z0-9]*\\/[A-Z][A-Za-z0-9]*$', // i18n `Namespace/Key` paths
|
|
17
|
+
'^data:', // Lottie and embedded images
|
|
18
|
+
];
|
|
9
19
|
export default function reactNative() {
|
|
10
20
|
return raiseWarnings([
|
|
11
21
|
{
|
|
@@ -16,6 +26,7 @@ export default function reactNative() {
|
|
|
16
26
|
{
|
|
17
27
|
name: '@hypersolid-dev/react-native',
|
|
18
28
|
files: CODE_FILES,
|
|
29
|
+
plugins: { 'no-secrets': noSecrets },
|
|
19
30
|
rules: {
|
|
20
31
|
// Browser globals React Native lacks, and the coercing number globals. Bad: `document.title`, `isNaN(value)`.
|
|
21
32
|
'no-restricted-globals': [
|
|
@@ -48,6 +59,30 @@ export default function reactNative() {
|
|
|
48
59
|
'unicorn/prefer-event-target': 'off',
|
|
49
60
|
// Metro emits CommonJS, so top-level await does not bundle. Bad: `await init();` at module scope.
|
|
50
61
|
'unicorn/prefer-top-level-await': 'off',
|
|
62
|
+
// A boolean reads as a claim; Reanimated's `SharedValue<boolean>` is read through `.value`. Bad: `const visible = useSharedValue(true)`.
|
|
63
|
+
'unicorn/consistent-boolean-name': [
|
|
64
|
+
'error',
|
|
65
|
+
{ ...REACT_BOOLEAN_NAME_OPTIONS, wrappers: { SharedValue: 'value' } },
|
|
66
|
+
],
|
|
67
|
+
// High-entropy strings that look like credentials, minus Expo and native identifiers. Bad: `const token = 'ghp_Zx8kQ2vLm9…'`.
|
|
68
|
+
'no-secrets/no-secrets': ['error', { ignoreContent: NO_SECRETS_IGNORE }],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: '@hypersolid-dev/react-native/json',
|
|
73
|
+
files: ['**/*.json'],
|
|
74
|
+
plugins: { 'no-secrets': noSecrets },
|
|
75
|
+
rules: {
|
|
76
|
+
// Credentials in JSON, minus letters-only words and Expo and native identifiers. Bad: `{ "token": "ghp_Zx8kQ2vLm9…" }`.
|
|
77
|
+
'no-secrets/no-secrets': ['error', { ignoreContent: ['^[A-Za-z]+$', ...NO_SECRETS_IGNORE] }],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: '@hypersolid-dev/react-native/tests',
|
|
82
|
+
files: TEST_FILES,
|
|
83
|
+
rules: {
|
|
84
|
+
// Off: tests stub browser globals. Bad: `document.title = 'x'` in a jsdom test reported.
|
|
85
|
+
'no-restricted-globals': 'off',
|
|
51
86
|
},
|
|
52
87
|
},
|
|
53
88
|
]);
|
package/dist/shared.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { Linter } from 'eslint';
|
|
2
2
|
export declare const CODE_FILES: string[];
|
|
3
|
+
export declare const TEST_FILES: string[];
|
|
4
|
+
export declare const BOOLEAN_NAME_OPTIONS: {
|
|
5
|
+
checkFunctions: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const REACT_BOOLEAN_NAME_OPTIONS: {
|
|
8
|
+
ignore: string[];
|
|
9
|
+
checkFunctions: string;
|
|
10
|
+
};
|
|
3
11
|
export declare const NUMBER_GLOBAL_RESTRICTIONS: {
|
|
4
12
|
name: string;
|
|
5
13
|
message: string;
|
package/dist/shared.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import eslintReact from '@eslint-react/eslint-plugin';
|
|
2
2
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
3
|
+
import unicorn from 'eslint-plugin-unicorn';
|
|
3
4
|
import hypersolid from './plugins/props-destructuring.js';
|
|
4
5
|
export const CODE_FILES = ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
|
|
6
|
+
// Tests, stories, mocks and test setup: each preset relaxes a few rules here that test doubles break by design.
|
|
7
|
+
export const TEST_FILES = [
|
|
8
|
+
'**/*.{test,spec,stories}.{js,mjs,cjs,jsx,ts,mts,cts,tsx}',
|
|
9
|
+
'**/__mocks__/**',
|
|
10
|
+
'**/{jest,vitest}.setup.*',
|
|
11
|
+
];
|
|
12
|
+
// `unicorn/consistent-boolean-name` options per preset; a later config replaces options, so each preset states them all.
|
|
13
|
+
export const BOOLEAN_NAME_OPTIONS = { checkFunctions: 'never' };
|
|
14
|
+
// Hooks (`useIsOpen`) and jest mock factories (`mockIsReady`) are named by convention, not as claims.
|
|
15
|
+
export const REACT_BOOLEAN_NAME_OPTIONS = { ...BOOLEAN_NAME_OPTIONS, ignore: ['^use[A-Z]', '^mock'] };
|
|
5
16
|
// Shared by every preset that sets `no-restricted-globals`: a later config replaces the rule's options, not merges them.
|
|
6
17
|
export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
|
|
7
18
|
name,
|
|
@@ -39,7 +50,7 @@ export function reactConfigs() {
|
|
|
39
50
|
{
|
|
40
51
|
name: '@hypersolid-dev/react',
|
|
41
52
|
files: CODE_FILES,
|
|
42
|
-
plugins: { '@hypersolid-dev': hypersolid },
|
|
53
|
+
plugins: { '@hypersolid-dev': hypersolid, unicorn },
|
|
43
54
|
languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
|
|
44
55
|
rules: {
|
|
45
56
|
// Off: react-hooks/exhaustive-deps reports it. Bad: `useEffect(() => log(id), [])` reported twice.
|
|
@@ -72,6 +83,16 @@ export function reactConfigs() {
|
|
|
72
83
|
'@eslint-react/no-unstable-context-value': 'error',
|
|
73
84
|
// Props are readable from the component's signature; a union props type may keep `props`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`.
|
|
74
85
|
'@hypersolid-dev/props-destructuring': 'error',
|
|
86
|
+
// A boolean reads as a claim; hook and mock names are exempt. Bad: `const visible = true` (use `isVisible`).
|
|
87
|
+
'unicorn/consistent-boolean-name': ['error', REACT_BOOLEAN_NAME_OPTIONS],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: '@hypersolid-dev/react/tests',
|
|
92
|
+
files: TEST_FILES,
|
|
93
|
+
rules: {
|
|
94
|
+
// Off: a mocked hook keeps the `use` name of the hook it replaces. Bad: `const useUser = () => fakeUser` in a mock.
|
|
95
|
+
'@eslint-react/no-unnecessary-use-prefix': 'off',
|
|
75
96
|
},
|
|
76
97
|
},
|
|
77
98
|
];
|