@hypersolid-dev/eslint-config 0.1.2 → 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 CHANGED
@@ -1,3 +1,26 @@
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
+
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)
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ * **react:** components that take `props` whole without a union props type
18
+ are now reported.
19
+
20
+ ### Features
21
+
22
+ * **react:** require props destructuring in component signatures ([36ad7fe](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/commit/36ad7fed0376a2e1842d9be4383a9d06fb930746)), closes [AB#5175](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_workitems/edit/5175)
23
+
1
24
  ## [0.1.2](https://dev.azure.com/wearetriple/Triple%20-%20Webteam/_git/eslint-config-hypersolid/branchCompare?baseVersion=GTv0.1.1&targetVersion=GTv0.1.2) (2026-11-06)
2
25
 
3
26
  ### Bug Fixes
package/README.md CHANGED
@@ -79,6 +79,14 @@ import react from '@hypersolid-dev/eslint-config/react';
79
79
  export default [...base({ tsconfigRootDir: import.meta.dirname }), ...react(), ...next()];
80
80
  ```
81
81
 
82
+ ## Custom rules
83
+
84
+ This package ships its own plugin, namespaced `@hypersolid-dev`, enabled in `react` and `react-native`.
85
+
86
+ | Rule | Enforces |
87
+ | --- | --- |
88
+ | `@hypersolid-dev/props-destructuring` | A component destructures its props in the signature, so its props are readable there. Replaces `react/destructuring-assignment` (`always`, `destructureInSignature: always`), as `eslint-plugin-react` doesn't support ESLint 10. Checks PascalCase functions that return JSX, including those wrapped in `memo` and `forwardRef`. Bad: `const Card = (props: Props) => <h1>{props.title}</h1>`, or `const { title } = props` in the body. Good: `const Card = ({ title }: Props) => …`. When the props type is a union, such as a discriminated union, `props` may stay whole, because destructuring breaks narrowing (`if (props.kind === 'a') props.a`). |
89
+
82
90
  ## Overriding rules
83
91
 
84
92
  Add your own config objects after the presets. A later object wins for the files it matches.
@@ -116,6 +124,22 @@ export default [...base({ tsconfigRootDir: import.meta.dirname }), { ignores: ['
116
124
 
117
125
  `base` already ignores `node_modules`, `dist`, `build`, `coverage` and lockfiles.
118
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
+
119
143
  ## Formatting and import order
120
144
 
121
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
@@ -1,4 +1,5 @@
1
1
  import type { Linter } from 'eslint';
2
+ export { TEST_FILES } from './shared.js';
2
3
  export type BaseOptions = {
3
4
  /** Directory of the consumer's tsconfig, usually `import.meta.dirname`. */
4
5
  tsconfigRootDir: string;
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
- // `sort()` needs a comparator; the default sorts numbers as strings. Bad: `[10, 9].sort()`.
70
- '@typescript-eslint/require-array-sort-compare': 'error',
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. Bad: `switch (shape) { case 'circle': … }` missing `'square'`.
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', { checkFunctions: 'never' }],
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`), not credentials.
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
  ]);
@@ -0,0 +1,3 @@
1
+ import type { ESLint } from 'eslint';
2
+ declare const plugin: ESLint.Plugin;
3
+ export default plugin;
@@ -0,0 +1,86 @@
1
+ const FUNCTION_TYPES = new Set(['ArrowFunctionExpression', 'FunctionExpression', 'FunctionDeclaration']);
2
+ const WRAPPERS = new Set(['memo', 'forwardRef']);
3
+ const isFunction = (node) => typeof node === 'object' && node !== null && FUNCTION_TYPES.has(node.type);
4
+ const isPascalCase = (name) => typeof name === 'string' && /^[A-Z]/.test(name);
5
+ function isJsx(node) {
6
+ if (typeof node !== 'object' || node === null) {
7
+ return false;
8
+ }
9
+ const { type, consequent, alternate, left, right } = node;
10
+ if (type === 'JSXElement' || type === 'JSXFragment') {
11
+ return true;
12
+ }
13
+ if (type === 'ConditionalExpression') {
14
+ return isJsx(consequent) || isJsx(alternate);
15
+ }
16
+ return type === 'LogicalExpression' && (isJsx(left) || isJsx(right));
17
+ }
18
+ // Searches the function's own return statements; nested functions return for themselves.
19
+ function returnsJsx(node) {
20
+ if (typeof node !== 'object' || node === null) {
21
+ return false;
22
+ }
23
+ if (Array.isArray(node)) {
24
+ return node.some(returnsJsx);
25
+ }
26
+ const current = node;
27
+ return current.type === 'ReturnStatement'
28
+ ? isJsx(current.argument)
29
+ : Object.entries(current).some(([key, value]) => key !== 'parent' && !isFunction(value) && typeof value === 'object' && returnsJsx(value));
30
+ }
31
+ // `memo(fn)`, `React.forwardRef(fn)` and `memo(forwardRef(fn))`.
32
+ function unwrap(node) {
33
+ if (isFunction(node)) {
34
+ return node;
35
+ }
36
+ const { type, callee, arguments: args } = (node ?? {});
37
+ if (type !== 'CallExpression') {
38
+ return undefined;
39
+ }
40
+ const { type: calleeType, name, property } = callee;
41
+ const calleeName = calleeType === 'MemberExpression' ? property.name : name;
42
+ return typeof calleeName === 'string' && WRAPPERS.has(calleeName) ? unwrap(args[0]) : undefined;
43
+ }
44
+ const propsDestructuring = {
45
+ meta: {
46
+ type: 'suggestion',
47
+ docs: { description: 'Require React components to destructure their props in the signature.' },
48
+ schema: [],
49
+ messages: {
50
+ destructure: 'Destructure the props in the signature: `({ title }: Props)`. A discriminated-union props type may keep `props`.',
51
+ },
52
+ },
53
+ create(context) {
54
+ const services = context.sourceCode.parserServices;
55
+ const checker = services.program?.getTypeChecker();
56
+ // Destructuring a union in the signature breaks narrowing (`if (props.kind === 'a') props.aOnly`).
57
+ const isUnion = (parameter) => {
58
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(parameter);
59
+ return checker !== undefined && tsNode !== undefined && checker.getNonNullableType(checker.getTypeAtLocation(tsNode)).isUnion();
60
+ };
61
+ const check = (fn) => {
62
+ const props = fn?.params[0];
63
+ if (!fn || props?.type !== 'Identifier' || !returnsJsx(fn.body) && !isJsx(fn.body) || isUnion(props)) {
64
+ return;
65
+ }
66
+ context.report({ node: props, messageId: 'destructure' });
67
+ };
68
+ return {
69
+ FunctionDeclaration(node) {
70
+ if (isPascalCase(node.id.name)) {
71
+ check(node);
72
+ }
73
+ },
74
+ VariableDeclarator(node) {
75
+ if (node.id.type === 'Identifier' && isPascalCase(node.id.name)) {
76
+ check(unwrap(node.init));
77
+ }
78
+ },
79
+ };
80
+ },
81
+ };
82
+ const plugin = {
83
+ meta: { name: '@hypersolid-dev/eslint-config' },
84
+ rules: { 'props-destructuring': propsDestructuring },
85
+ };
86
+ export default plugin;
@@ -1,11 +1,21 @@
1
1
  import globals from 'globals';
2
- import { CODE_FILES, NUMBER_GLOBAL_RESTRICTIONS, raiseWarnings, reactConfigs } from './shared.js';
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,6 +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';
4
+ import hypersolid from './plugins/props-destructuring.js';
3
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'] };
4
16
  // Shared by every preset that sets `no-restricted-globals`: a later config replaces the rule's options, not merges them.
5
17
  export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
6
18
  name,
@@ -38,6 +50,7 @@ export function reactConfigs() {
38
50
  {
39
51
  name: '@hypersolid-dev/react',
40
52
  files: CODE_FILES,
53
+ plugins: { '@hypersolid-dev': hypersolid, unicorn },
41
54
  languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
42
55
  rules: {
43
56
  // Off: react-hooks/exhaustive-deps reports it. Bad: `useEffect(() => log(id), [])` reported twice.
@@ -68,6 +81,18 @@ export function reactConfigs() {
68
81
  '@eslint-react/no-array-index-key': 'error',
69
82
  // A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
70
83
  '@eslint-react/no-unstable-context-value': 'error',
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>`.
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',
71
96
  },
72
97
  },
73
98
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersolid-dev/eslint-config",
3
- "version": "0.1.2",
3
+ "version": "2.0.0",
4
4
  "description": "Shared ESLint flat config for Hypersolid TypeScript projects",
5
5
  "license": "MIT",
6
6
  "keywords": [