@hypersolid-dev/eslint-config 0.1.2 → 1.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,14 @@
1
+ ## [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
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * **react:** components that take `props` whole without a union props type
6
+ are now reported.
7
+
8
+ ### Features
9
+
10
+ * **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)
11
+
1
12
  ## [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
13
 
3
14
  ### 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.
@@ -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;
package/dist/shared.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import eslintReact from '@eslint-react/eslint-plugin';
2
2
  import reactHooks from 'eslint-plugin-react-hooks';
3
+ import hypersolid from './plugins/props-destructuring.js';
3
4
  export const CODE_FILES = ['**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx}'];
4
5
  // Shared by every preset that sets `no-restricted-globals`: a later config replaces the rule's options, not merges them.
5
6
  export const NUMBER_GLOBAL_RESTRICTIONS = ['isFinite', 'isNaN'].map((name) => ({
@@ -38,6 +39,7 @@ export function reactConfigs() {
38
39
  {
39
40
  name: '@hypersolid-dev/react',
40
41
  files: CODE_FILES,
42
+ plugins: { '@hypersolid-dev': hypersolid },
41
43
  languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } },
42
44
  rules: {
43
45
  // Off: react-hooks/exhaustive-deps reports it. Bad: `useEffect(() => log(id), [])` reported twice.
@@ -68,6 +70,8 @@ export function reactConfigs() {
68
70
  '@eslint-react/no-array-index-key': 'error',
69
71
  // A new context value each render re-renders every consumer. Bad: `<Context value={{ user }}>`.
70
72
  '@eslint-react/no-unstable-context-value': 'error',
73
+ // 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
+ '@hypersolid-dev/props-destructuring': 'error',
71
75
  },
72
76
  },
73
77
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypersolid-dev/eslint-config",
3
- "version": "0.1.2",
3
+ "version": "1.0.0",
4
4
  "description": "Shared ESLint flat config for Hypersolid TypeScript projects",
5
5
  "license": "MIT",
6
6
  "keywords": [