@smartive/eslint-config 8.0.0-next.3 → 8.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/README.md CHANGED
@@ -141,6 +141,33 @@ Enabled in the `react` and `nextjs` rule sets as:
141
141
  'smartive/forbid-component-props': ['warn', { forbid: ['style', 'className'] }]
142
142
  ```
143
143
 
144
+ Each entry in `forbid` is either a prop name or an object that narrows where the prop stays allowed:
145
+
146
+ | Key | Meaning |
147
+ | -------------------- | ---------------------------------------------------------------------------------------- |
148
+ | `propName` | The forbidden prop (required). |
149
+ | `allowedFor` | Component names the prop is still allowed on, matched exactly. |
150
+ | `allowedForPatterns` | Globs (`*` for any run of characters, `?` for one) the component name may match instead. |
151
+ | `message` | Replaces the default report message. |
152
+
153
+ Component names are matched on their full JSX name, so `<Mantine.Button />` matches `Mantine.*`.
154
+
155
+ ```javascript
156
+ 'smartive/forbid-component-props': [
157
+ 'error',
158
+ {
159
+ forbid: [
160
+ {
161
+ propName: 'className',
162
+ allowedFor: ['NextImage', 'NextLink'],
163
+ allowedForPatterns: ['*Icon', 'Mantine*'],
164
+ message: 'Avoid using className except NextImage, NextLink, icons and Mantine components',
165
+ },
166
+ ],
167
+ },
168
+ ]
169
+ ```
170
+
144
171
  ## Development
145
172
 
146
173
  ```sh
@@ -11,6 +11,27 @@ const isComponent = (name) => {
11
11
  return false;
12
12
  }
13
13
  };
14
+ /**
15
+ * The source text of the element name, so that `<Ui.Group.Item />` can be matched as `Ui.Group.Item`.
16
+ */
17
+ const elementName = (name) => {
18
+ switch (name.type) {
19
+ case 'JSXIdentifier':
20
+ return name.name;
21
+ case 'JSXMemberExpression':
22
+ return `${elementName(name.object)}.${name.property.name}`;
23
+ default:
24
+ return `${name.namespace.name}:${name.name.name}`;
25
+ }
26
+ };
27
+ /**
28
+ * Glob matching for `allowedForPatterns`: `*` matches any run of characters, `?` a single one.
29
+ */
30
+ const globToRegExp = (pattern) => new RegExp(`^${pattern
31
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
32
+ .replace(/\*/g, '.*')
33
+ .replace(/\?/g, '.')}$`);
34
+ const normalize = (option) => (typeof option === 'string' ? { propName: option } : option);
14
35
  /**
15
36
  * Replacement for `react/forbid-component-props`, which has no equivalent in
16
37
  * `@eslint-react/eslint-plugin` (https://eslint-react.xyz/docs/migrating-from-eslint-plugin-react).
@@ -28,7 +49,22 @@ export const forbidComponentProps = {
28
49
  properties: {
29
50
  forbid: {
30
51
  type: 'array',
31
- items: { type: 'string' },
52
+ items: {
53
+ anyOf: [
54
+ { type: 'string' },
55
+ {
56
+ type: 'object',
57
+ properties: {
58
+ propName: { type: 'string' },
59
+ allowedFor: { type: 'array', items: { type: 'string' }, uniqueItems: true },
60
+ allowedForPatterns: { type: 'array', items: { type: 'string' }, uniqueItems: true },
61
+ message: { type: 'string' },
62
+ },
63
+ required: ['propName'],
64
+ additionalProperties: false,
65
+ },
66
+ ],
67
+ },
32
68
  uniqueItems: true,
33
69
  },
34
70
  },
@@ -37,11 +73,19 @@ export const forbidComponentProps = {
37
73
  ],
38
74
  messages: {
39
75
  forbiddenProp: 'Prop "{{prop}}" is forbidden on components.',
76
+ forbiddenPropWithMessage: '{{message}}',
40
77
  },
41
78
  },
42
79
  create(context) {
43
80
  const { forbid = [] } = (context.options[0] ?? {});
44
- const forbidden = new Set(forbid);
81
+ const forbidden = new Map(forbid.map(normalize).map((entry) => [
82
+ entry.propName,
83
+ {
84
+ allowedFor: new Set(entry.allowedFor ?? []),
85
+ allowedForPatterns: (entry.allowedForPatterns ?? []).map(globToRegExp),
86
+ message: entry.message,
87
+ },
88
+ ]));
45
89
  if (forbidden.size === 0) {
46
90
  return {};
47
91
  }
@@ -51,17 +95,24 @@ export const forbidComponentProps = {
51
95
  if (!isComponent(element.name)) {
52
96
  return;
53
97
  }
98
+ const component = elementName(element.name);
54
99
  for (const attribute of element.attributes) {
55
100
  if (attribute.type !== 'JSXAttribute' || attribute.name.type !== 'JSXIdentifier') {
56
101
  continue;
57
102
  }
58
- if (forbidden.has(attribute.name.name)) {
59
- context.report({
60
- node: attribute,
61
- messageId: 'forbiddenProp',
62
- data: { prop: attribute.name.name },
63
- });
103
+ const entry = forbidden.get(attribute.name.name);
104
+ if (!entry) {
105
+ continue;
106
+ }
107
+ if (entry.allowedFor.has(component) || entry.allowedForPatterns.some((pattern) => pattern.test(component))) {
108
+ continue;
64
109
  }
110
+ context.report({
111
+ node: attribute,
112
+ ...(entry.message
113
+ ? { messageId: 'forbiddenPropWithMessage', data: { message: entry.message } }
114
+ : { messageId: 'forbiddenProp', data: { prop: attribute.name.name } }),
115
+ });
65
116
  }
66
117
  },
67
118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartive/eslint-config",
3
- "version": "8.0.0-next.3",
3
+ "version": "8.1.0",
4
4
  "description": "ESLint configuration by smartive",
5
5
  "files": [
6
6
  "README.md",
@@ -55,15 +55,15 @@
55
55
  "@commitlint/cli": "21.2.2",
56
56
  "@commitlint/config-conventional": "21.2.2",
57
57
  "@smartive/prettier-config": "3.1.2",
58
- "@types/node": "25.9.6",
58
+ "@types/node": "25.9.7",
59
59
  "@types/react": "19.3.0",
60
60
  "@types/react-dom": "19.3.0",
61
61
  "cz-conventional-changelog": "3.3.0",
62
- "eslint": "10.9.1",
62
+ "eslint": "10.10.0",
63
63
  "eslint-config-next": "16.3.5",
64
64
  "husky": "9.1.7",
65
65
  "next": "16.3.5",
66
- "prettier": "3.9.6",
66
+ "prettier": "3.9.8",
67
67
  "react": "19.3.0",
68
68
  "react-dom": "19.3.0",
69
69
  "typescript": "6.0.3"