@hero-design/eslint-plugin 9.0.1 → 9.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @hero-design/eslint-plugin
2
2
 
3
+ ## 9.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#3197](https://github.com/Thinkei/hero-design/pull/3197) [`36c71a4a7`](https://github.com/Thinkei/hero-design/commit/36c71a4a7719bd5f09925a9d88e197135696d0c9) Thanks [@vinhphan-eh](https://github.com/vinhphan-eh)! - Add react-no-text-outside-typography rule for react
8
+
3
9
  ## 9.0.1
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,31 @@
1
+ const { FlatCompat } = require('@eslint/eslintrc');
2
+ const js = require('@eslint/js');
3
+
4
+ const compat = new FlatCompat({
5
+ baseDirectory: __dirname,
6
+ recommendedConfig: js.configs.recommended,
7
+ });
8
+
9
+ module.exports = [
10
+ ...compat.extends(
11
+ 'eslint:recommended',
12
+ 'plugin:eslint-plugin/recommended',
13
+ 'plugin:node/recommended'
14
+ ),
15
+ {
16
+ files: ['**/*.js', '**/*.ts'],
17
+ languageOptions: {
18
+ globals: {
19
+ node: true,
20
+ },
21
+ },
22
+ },
23
+ {
24
+ files: ['tests/**/*.js'],
25
+ languageOptions: {
26
+ globals: {
27
+ jest: true,
28
+ },
29
+ },
30
+ },
31
+ ];
package/lib/index.js CHANGED
@@ -121,7 +121,8 @@ module.exports = {
121
121
  recommendedReact: {
122
122
  plugins: ['@hero-design'],
123
123
  rules: {
124
- '@hero-design/no-direct-color-palette-access': 'error'
124
+ '@hero-design/no-direct-color-palette-access': 'error',
125
+ '@hero-design/react-no-text-outside-typography': 'warn',
125
126
  }
126
127
  }
127
128
  },
@@ -0,0 +1,216 @@
1
+ const notRecommendedList = [
2
+ 'div',
3
+ 'p',
4
+ 'span',
5
+ 'h1',
6
+ 'h2',
7
+ 'h3',
8
+ 'h4',
9
+ 'h5',
10
+ 'h6',
11
+ 'a',
12
+ 'strong',
13
+ 'em',
14
+ 'b',
15
+ 'i',
16
+ 'u',
17
+ ];
18
+
19
+ // Get the parent tag name of the node
20
+ const getParentTagName = (node) => {
21
+ let parent = node.parent;
22
+ while (parent) {
23
+ if (parent.type === 'JSXElement') {
24
+ return parent.openingElement.name.name;
25
+ }
26
+ parent = parent.parent;
27
+ }
28
+ return null;
29
+ };
30
+
31
+ // Find the Typography component in the parent hierarchy
32
+ // and return the value of tagName prop and the level of nested component
33
+ const findTypography = (node, typographyNames) => {
34
+ let parent = node.parent;
35
+ let level = 0;
36
+ let tagName = null;
37
+ let found = false;
38
+
39
+ while (parent) {
40
+ if (
41
+ parent.type === 'JSXElement' &&
42
+ parent.openingElement.name.type === 'JSXMemberExpression' &&
43
+ typographyNames.includes(parent.openingElement.name.object.name)
44
+ ) {
45
+ found = true;
46
+ const attributes = parent.openingElement.attributes;
47
+ for (let attr of attributes) {
48
+ if (attr.name && attr.name.name === 'tagName') {
49
+ tagName = attr.value.value;
50
+ }
51
+ }
52
+ break;
53
+ }
54
+ parent = parent.parent;
55
+ level++;
56
+ }
57
+ return { tagName, level, found };
58
+ };
59
+
60
+ // Check if the direct parent element is in the notRecommendedList
61
+ const isParentNotRecommended = (node) => {
62
+ let parentElement = node.parent;
63
+ while (parentElement && parentElement.type !== 'JSXElement') {
64
+ parentElement = parentElement.parent;
65
+ }
66
+
67
+ return (
68
+ parentElement &&
69
+ parentElement.openingElement.name.type === 'JSXIdentifier' &&
70
+ notRecommendedList.includes(parentElement.openingElement.name.name)
71
+ );
72
+ };
73
+
74
+ const checkNode = (node, context, typographyNames) => {
75
+ const {
76
+ tagName,
77
+ level,
78
+ found: foundTypography,
79
+ } = findTypography(node, typographyNames);
80
+
81
+ // Typography not found, check if the direct parent is in the notRecommendedList
82
+ if (!foundTypography && isParentNotRecommended(node)) {
83
+ context.report({
84
+ node,
85
+ messageId: 'textNodeOutsideTypography',
86
+ });
87
+ }
88
+
89
+ // Typography found, handle edge cases as documented in
90
+ // https://design.employmenthero.com/web/Components/Typography/#typographytext
91
+ if (foundTypography) {
92
+ switch (tagName) {
93
+ case 'p':
94
+ case '':
95
+ case 'span':
96
+ case 'label': {
97
+ if (level > 1) {
98
+ // Not allowing nested of nested nodes
99
+ context.report({
100
+ node,
101
+ messageId: 'textNodeOutsideTypography',
102
+ });
103
+ }
104
+ break;
105
+ }
106
+ case 'div': {
107
+ // Allowed cases:
108
+ // 2 level nested with unordered list
109
+ // 2 level nested elements with p
110
+ const parentTagName = getParentTagName(node);
111
+ const grandParentTagName = getParentTagName(node.parent);
112
+
113
+ const isAllowedUnorderedList =
114
+ level === 2 && parentTagName === 'li' && grandParentTagName === 'ul';
115
+
116
+ const isAllowedParagraph = level === 2 && grandParentTagName === 'p';
117
+
118
+ if (!isAllowedParagraph && !isAllowedUnorderedList && level > 1) {
119
+ context.report({
120
+ node,
121
+ messageId: 'textNodeOutsideTypography',
122
+ });
123
+ }
124
+ break;
125
+ }
126
+ }
127
+ }
128
+ };
129
+
130
+ function containsIntlFormatMessage(expression) {
131
+ if (expression === null || expression === undefined) {
132
+ return false;
133
+ }
134
+
135
+ if (
136
+ expression.type === 'CallExpression' &&
137
+ expression.callee.type === 'MemberExpression' &&
138
+ expression.callee.object.name === 'Intl' &&
139
+ expression.callee.property.name === 'formatMessage'
140
+ ) {
141
+ return true;
142
+ }
143
+
144
+ // Example: {condition1 && Intl.formatMessage({ id: 'someId' })}
145
+ // Also works for multiple logical expressions
146
+ if (expression.type === 'LogicalExpression') {
147
+ return (
148
+ containsIntlFormatMessage(expression.left) ||
149
+ containsIntlFormatMessage(expression.right)
150
+ );
151
+ }
152
+
153
+ if (expression.type === 'ConditionalExpression') {
154
+ return (
155
+ containsIntlFormatMessage(expression.consequent) ||
156
+ containsIntlFormatMessage(expression.alternate)
157
+ );
158
+ }
159
+
160
+ return false;
161
+ }
162
+
163
+ module.exports = {
164
+ meta: {
165
+ type: 'problem',
166
+ docs: {
167
+ description: 'Recommend using text inside Typography component.',
168
+ recommended: false,
169
+ },
170
+ schema: [],
171
+ messages: {
172
+ textNodeOutsideTypography:
173
+ 'Text nodes should be inside Typography component.',
174
+ },
175
+ },
176
+ create(context) {
177
+ let typographyNames = ['Typography'];
178
+
179
+ return {
180
+ ImportDeclaration(node) {
181
+ if (node.source.value === '@hero-design/react') {
182
+ node.specifiers.forEach((specifier) => {
183
+ if (
184
+ specifier.imported &&
185
+ specifier.imported.name === 'Typography'
186
+ ) {
187
+ specifier.local.name !== 'Typography' &&
188
+ typographyNames.push(specifier.local.name);
189
+ }
190
+ });
191
+ }
192
+ },
193
+ VariableDeclarator(node) {
194
+ // Handle local assignments
195
+ if (
196
+ node.init &&
197
+ node.init.type === 'Identifier' &&
198
+ typographyNames.includes(node.init.name)
199
+ ) {
200
+ node.id.name !== 'Typography' && typographyNames.push(node.id.name);
201
+ }
202
+ },
203
+ JSXText(node) {
204
+ const trimmedValue = node.value.trim();
205
+ if (trimmedValue) {
206
+ checkNode(node, context, typographyNames);
207
+ }
208
+ },
209
+ JSXExpressionContainer(node) {
210
+ if (containsIntlFormatMessage(node.expression)) {
211
+ checkNode(node, context, typographyNames);
212
+ }
213
+ },
214
+ };
215
+ },
216
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hero-design/eslint-plugin",
3
- "version": "9.0.1",
3
+ "version": "9.1.0",
4
4
  "description": "Hero Design's eslint plugin",
5
5
  "keywords": [
6
6
  "eslint",
@@ -12,7 +12,7 @@
12
12
  "exports": "./lib/index.js",
13
13
  "prettier": "prettier-config-hd",
14
14
  "scripts": {
15
- "lint": "eslint .",
15
+ "lint": "eslint lib tests",
16
16
  "test": "jest --runInBand",
17
17
  "test:watch": "jest --runInBand --watch",
18
18
  "test:ci": "jest --runInBand --logHeapUsage",
@@ -22,7 +22,10 @@
22
22
  "requireindex": "^1.2.0"
23
23
  },
24
24
  "devDependencies": {
25
- "eslint": "^8.10.0",
25
+ "@eslint/compat": "^1.1.1",
26
+ "@eslint/eslintrc": "^3.1.0",
27
+ "@eslint/js": "^9.8.0",
28
+ "eslint": "^8.56.0",
26
29
  "eslint-plugin-eslint-plugin": "^5.0.0",
27
30
  "eslint-plugin-node": "^11.1.0",
28
31
  "jest": "^29.2.1",
@@ -0,0 +1,248 @@
1
+ const rule = require('../../../lib/rules/react-no-text-outside-typography');
2
+ const RuleTester = require('eslint').RuleTester;
3
+
4
+ //------------------------------------------------------------------------------
5
+ // Tests
6
+ //------------------------------------------------------------------------------
7
+
8
+ const config = {
9
+ parserOptions: {
10
+ sourceType: 'module',
11
+ ecmaVersion: 6,
12
+ ecmaFeatures: { jsx: true },
13
+ },
14
+ };
15
+
16
+ const ruleTester = new RuleTester();
17
+ ruleTester.run('react-no-text-outside-typography', rule, {
18
+ valid: [
19
+ // Common cases
20
+ {
21
+ code: '<Typography.Text>Content</Typography.Text>',
22
+ },
23
+ {
24
+ code: '<Typography.Title>Content</Typography.Title>',
25
+ },
26
+ {
27
+ code: `<Typography.Text>
28
+ Click <a href='..'>here</a>
29
+ </Typography.Text>`,
30
+ },
31
+ {
32
+ code: `<Typography.Text>
33
+ <Box>Content</Box>
34
+ </Typography.Text>
35
+ `,
36
+ },
37
+ // With Intl.formatMessage and conditions
38
+ {
39
+ code: `<Typography.Text>
40
+ <Box>{Intl.formatMessage({ id: 'someId' })}</Box>
41
+ </Typography.Text>
42
+ `,
43
+ },
44
+ {
45
+ code: `<Typography.Text>
46
+ <Box>{condition1 && Intl.formatMessage({ id: 'someId' })}</Box>
47
+ </Typography.Text>
48
+ `,
49
+ },
50
+ {
51
+ code: `<Typography.Text>
52
+ <Box>{condition1 ? Intl.formatMessage({ id: 'someId' }) : null}</Box>
53
+ </Typography.Text>
54
+ `,
55
+ },
56
+ {
57
+ code: `
58
+ <Typography.Text>
59
+ {Intl.formatMessage({ id: 'someId' })}
60
+ </Typography.Text>`,
61
+ },
62
+ {
63
+ code: `
64
+ <Typography.Text>
65
+ {condition1 && Intl.formatMessage({ id: 'someId' })}
66
+ </Typography.Text>`,
67
+ },
68
+ {
69
+ code: `
70
+ <Typography.Text>
71
+ {condition1 ? Intl.formatMessage({ id: 'someId' }) : null}
72
+ </Typography.Text>`,
73
+ },
74
+ {
75
+ code: `
76
+ <Typography.Text>
77
+ {condition1 ? Intl.formatMessage({ id: 'someId' }) : Intl.formatMessage({ id: 'someId2' })}
78
+ </Typography.Text>`,
79
+ },
80
+ // Custom namings
81
+ {
82
+ code: `
83
+ import { Typography as HDTypography } from '@hero-design/react';
84
+ <HDTypography.Text>Content</HDTypography.Text>
85
+ `,
86
+ },
87
+ {
88
+ code: `
89
+ import { Typography as HDTypography } from '@hero-design/react';
90
+ const { Text } = HDTypography;
91
+ <Text>Content</Text>
92
+ `,
93
+ },
94
+ {
95
+ code: `
96
+ import { Typography } from '@hero-design/react';
97
+ const { Title } = Typography;
98
+ <Title>Content</Title>
99
+ `,
100
+ },
101
+ {
102
+ code: `
103
+ import { Typography } from '@hero-design/react';
104
+ const { Title: HDTypographyTitle } = Typography;
105
+ <HDTypographyTitle>Content</HDTypographyTitle>
106
+ `,
107
+ },
108
+ {
109
+ code: `
110
+ import { Typography } from '@hero-design/react';
111
+ const Title = Typography.Title;
112
+ <Title>Content</Title>
113
+ `,
114
+ },
115
+ // Exceptions with tagName prop
116
+ {
117
+ code: `
118
+ <Typography.Text tagName='p'>
119
+ <span>Content</span>
120
+ </Typography.Text>
121
+ `,
122
+ },
123
+ {
124
+ code: `
125
+ <Typography.Text tagName='div'>
126
+ <ul>
127
+ <li>Content</li>
128
+ </ul>
129
+ </Typography.Text>
130
+ `,
131
+ },
132
+ {
133
+ code: `
134
+ <Typography.Text tagName='div'>
135
+ <p>
136
+ <a>Content</a>
137
+ </p>
138
+ </Typography.Text>
139
+ `,
140
+ },
141
+ {
142
+ code: `
143
+ <Typography.Text tagName='span'>
144
+ <span>Content</span>
145
+ </Typography.Text>
146
+ `,
147
+ },
148
+ {
149
+ code: `
150
+ <Typography.Text tagName='label'>
151
+ <span>Content</span>
152
+ </Typography.Text>
153
+ `,
154
+ },
155
+ ].map((test) => ({ ...test, ...config })),
156
+ invalid: [
157
+ // Common cases
158
+ {
159
+ code: '<div>Content</div>',
160
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
161
+ },
162
+ {
163
+ code: '<p>Content</p>',
164
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
165
+ },
166
+ {
167
+ code: '<h1>Content</h1>',
168
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
169
+ },
170
+ // With Intl.formatMessage and conditions
171
+ {
172
+ code: `
173
+ <div>
174
+ {Intl.formatMessage({ id: 'someId' })}
175
+ </div>`,
176
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
177
+ },
178
+ {
179
+ code: `
180
+ <div>
181
+ { condition && Intl.formatMessage({ id: 'someId' })}
182
+ </div>`,
183
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
184
+ },
185
+ {
186
+ code: `
187
+ <div>
188
+ { condition && condition2 && Intl.formatMessage({ id: 'someId' })}
189
+ </div>`,
190
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
191
+ },
192
+ {
193
+ code: `
194
+ <div>
195
+ { condition && condition2 || condition3 && Intl.formatMessage({ id: 'someId' })}
196
+ </div>`,
197
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
198
+ },
199
+ {
200
+ code: `
201
+ <div>
202
+ {condition1 ? Intl.formatMessage({ id: 'someId' }) : null}
203
+ </div>`,
204
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
205
+ },
206
+ // Exceptions with tagName prop
207
+ {
208
+ code: `
209
+ <Typography.Text tagName='p'>
210
+ <span>
211
+ <span>Content</span>
212
+ </span>
213
+ </Typography.Text>
214
+ `,
215
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
216
+ },
217
+ {
218
+ code: `
219
+ <Typography.Text tagName='div'>
220
+ <span>
221
+ <li>Content</li>
222
+ </span>
223
+ </Typography.Text>
224
+ `,
225
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
226
+ },
227
+ {
228
+ code: `
229
+ <Typography.Text tagName='span'>
230
+ <span>
231
+ <span>Content</span>
232
+ </span>
233
+ </Typography.Text>
234
+ `,
235
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
236
+ },
237
+ {
238
+ code: `
239
+ <Typography.Text tagName='label'>
240
+ <span>
241
+ <span>Content</span>
242
+ </span>
243
+ </Typography.Text>
244
+ `,
245
+ errors: [{ messageId: 'textNodeOutsideTypography' }],
246
+ },
247
+ ].map((test) => ({ ...test, ...config })),
248
+ });
package/.eslintrc.js DELETED
@@ -1,19 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = {
4
- root: true,
5
- extends: [
6
- 'eslint:recommended',
7
- 'plugin:eslint-plugin/recommended',
8
- 'plugin:node/recommended',
9
- ],
10
- env: {
11
- node: true,
12
- },
13
- overrides: [
14
- {
15
- files: ['tests/**/*.js'],
16
- env: { jest: true },
17
- },
18
- ],
19
- };