@hero-design/eslint-plugin 9.2.1-rc.0 → 9.2.2
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 +43 -0
- package/README.md +125 -15
- package/docs/rules/banning-snowflake-approve-comment.md +42 -0
- package/docs/rules/no-deprecated-component-prop-next.md +32 -0
- package/docs/rules/no-direct-color-palette-access.md +34 -0
- package/docs/rules/react-no-text-outside-typography.md +57 -0
- package/eslint.config.js +31 -0
- package/lib/index.js +15 -298
- package/lib/rules/banning-snowflake-approve-comment.js +32 -0
- package/lib/rules/no-deprecated-component-prop-next.js +3 -0
- package/lib/rules/no-deprecated-theme-key.js +3 -15
- package/lib/rules/no-direct-color-palette-access.js +56 -0
- package/lib/rules/react-no-text-outside-typography.js +216 -0
- package/lib/utils.js +17 -0
- package/package.json +8 -6
- package/tests/lib/rules/banning-snowflake-approve-comment.js +46 -0
- package/tests/lib/rules/no-direct-color-palette-access.js +45 -0
- package/tests/lib/rules/react-no-text-outside-typography.js +248 -0
- package/.eslintrc.js +0 -19
- package/.turbo/turbo-publish:npm.log +0 -0
package/lib/index.js
CHANGED
|
@@ -14,213 +14,6 @@ const requireIndex = require('requireindex');
|
|
|
14
14
|
// Plugin Definition
|
|
15
15
|
//------------------------------------------------------------------------------
|
|
16
16
|
|
|
17
|
-
const themeKeysMap = [
|
|
18
|
-
{
|
|
19
|
-
old: 'colors.globalPrimary',
|
|
20
|
-
new: 'colors.onDefaultGlobalSurface',
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
old: 'colors.globalPrimaryLight',
|
|
24
|
-
new: 'colors.globalPrimaryLight',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
old: 'colors.globalPrimaryBackground',
|
|
28
|
-
new: 'colors.defaultGlobalSurface',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
old: 'colors.primaryLight',
|
|
32
|
-
new: 'colors.secondary',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
old: 'colors.primaryDark',
|
|
36
|
-
new: 'colors.primary',
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
old: 'colors.primaryBackground',
|
|
40
|
-
new: 'colors.highlightedSurface',
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
old: 'colors.primaryBackgroundDark',
|
|
44
|
-
new: 'colors.mutedOnDefaultGlobalSurface',
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
old: 'colors.secondaryLight',
|
|
48
|
-
new: 'colors.secondaryLight',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
old: 'colors.secondaryBackground',
|
|
52
|
-
new: 'colors.secondaryBackground',
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
old: 'colors.infoMediumLight',
|
|
56
|
-
new: 'colors.mutedInfo',
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
old: 'colors.infoLight',
|
|
60
|
-
new: 'colors.mutedInfo',
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
old: 'colors.infoBackground',
|
|
64
|
-
new: 'colors.infoSurface',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
old: 'colors.successLight',
|
|
68
|
-
new: 'colors.mutedSuccess',
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
old: 'colors.successDark',
|
|
72
|
-
new: 'colors.success',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
old: 'colors.successBackground',
|
|
76
|
-
new: 'colors.successSurface',
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
old: 'colors.danger',
|
|
80
|
-
new: 'colors.error',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
old: 'colors.dangerMediumLight',
|
|
84
|
-
new: 'colors.mutedError',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
old: 'colors.dangerLight',
|
|
88
|
-
new: 'colors.mutedError',
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
old: 'colors.dangerBackground',
|
|
92
|
-
new: 'colors.errorSurface',
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
old: 'colors.warningLight',
|
|
96
|
-
new: 'colors.mutedWarning',
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
old: 'colors.warningDark',
|
|
100
|
-
new: 'colors.warning',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
old: 'colors.warningBackground',
|
|
104
|
-
new: 'colors.warningSurface',
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
old: 'colors.platformBackground',
|
|
108
|
-
new: 'colors.defaultGlobalSurface',
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
old: 'colors.backgroundLight',
|
|
112
|
-
new: 'colors.neutralGlobalSurface',
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
old: 'colors.backgroundDark',
|
|
116
|
-
new: 'colors.darkGlobalSurface',
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
old: 'colors.text',
|
|
120
|
-
new: 'colors.onDefaultGlobalSurface',
|
|
121
|
-
},
|
|
122
|
-
{
|
|
123
|
-
old: 'colors.subduedText',
|
|
124
|
-
new: 'colors.mutedOnDefaultGlobalSurface',
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
old: 'colors.disabledText',
|
|
128
|
-
new: 'colors.inactiveOnDefaultGlobalSurface',
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
old: 'colors.disabledLightText',
|
|
132
|
-
new: 'colors.disabledOnDefaultGlobalSurface',
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
old: 'colors.invertedText',
|
|
136
|
-
new: 'colors.onDarkGlobalSurface',
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
old: 'colors.outline',
|
|
140
|
-
new: 'colors.secondaryOutline',
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
old: 'colors.archivedLight',
|
|
144
|
-
new: 'colors.disabledOnDefaultGlobalSurface',
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
old: 'colors.archivedDark',
|
|
148
|
-
new: 'colors.archived',
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
old: 'colors.archivedBackground',
|
|
152
|
-
new: 'colors.archivedSurface',
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
old: 'colors.black',
|
|
156
|
-
new: 'colors.onDefaultGlobalSurface',
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
old: 'colors.inactiveBackground',
|
|
160
|
-
new: 'colors.inactiveOnDefaultGlobalSurface',
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
old: 'colors.shadow',
|
|
164
|
-
new: 'colors.secondaryOutline',
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
old: 'colors.mutedGlobalPrimary',
|
|
168
|
-
new: 'colors.mutedOnDefaultGlobalSurface',
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
old: 'colors.onGlobalPrimary',
|
|
172
|
-
new: 'colors.onDefaultGlobalSurface',
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
old: 'colors.globalSecondary',
|
|
176
|
-
new: 'colors.secondary',
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
old: 'colors.globalPrimaryOutline',
|
|
180
|
-
new: 'colors.primaryOutline',
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
old: 'colors.globalSecondaryOutline',
|
|
184
|
-
new: 'colors.secondaryOutline',
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
old: 'colors.mutedPrimary',
|
|
188
|
-
new: 'colors.mutedPrimary',
|
|
189
|
-
},
|
|
190
|
-
{
|
|
191
|
-
old: 'colors.highlightedSecondarySurface',
|
|
192
|
-
new: 'colors.highlightedSecondarySurface',
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
old: 'colors.mutedSecondary',
|
|
196
|
-
new: 'colors.mutedSecondary',
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
old: 'colors.disabledSecondary',
|
|
200
|
-
new: 'colors.disabledSecondary',
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
old: 'colors.lightHighlightedSurface',
|
|
204
|
-
new: 'colors.highlightedSurface',
|
|
205
|
-
},
|
|
206
|
-
];
|
|
207
|
-
const deprecatedThemeColors = themeKeysMap.map((c) => c.old.split('.')[1]);
|
|
208
|
-
const boxColorProps = [
|
|
209
|
-
'backgroundColor',
|
|
210
|
-
'bgColor',
|
|
211
|
-
'borderColor',
|
|
212
|
-
'borderTopColor',
|
|
213
|
-
'borderBottomColor',
|
|
214
|
-
'borderStartColor',
|
|
215
|
-
'borderEndColor',
|
|
216
|
-
'borderLeftColor',
|
|
217
|
-
'borderRightColor',
|
|
218
|
-
];
|
|
219
|
-
const deprecatedBoxPropValues = boxColorProps.map((prop) => ({
|
|
220
|
-
name: prop,
|
|
221
|
-
values: deprecatedThemeColors,
|
|
222
|
-
}));
|
|
223
|
-
|
|
224
17
|
// import all rules in lib/rules
|
|
225
18
|
module.exports = {
|
|
226
19
|
rules: requireIndex(__dirname + '/rules'),
|
|
@@ -228,101 +21,18 @@ module.exports = {
|
|
|
228
21
|
internalRn: {
|
|
229
22
|
plugins: ['@hero-design'],
|
|
230
23
|
rules: {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
keys: themeKeysMap,
|
|
235
|
-
},
|
|
236
|
-
],
|
|
237
|
-
'@hero-design/no-deprecated-component-prop': [
|
|
238
|
-
'error',
|
|
239
|
-
{
|
|
240
|
-
package: '@hero-design/rn',
|
|
241
|
-
components: [
|
|
242
|
-
{ name: 'Card', props: ['variant'] },
|
|
243
|
-
{ name: 'Switch', props: ['size'] },
|
|
244
|
-
{
|
|
245
|
-
name: 'Select',
|
|
246
|
-
props: ['onDimiss', 'numberOfLines', 'inputProps.required'],
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
name: 'Select.Multi',
|
|
250
|
-
props: ['onDimiss', 'numberOfLines', 'inputProps.required'],
|
|
251
|
-
},
|
|
252
|
-
{ name: 'Toast.Provider', props: ['position'] },
|
|
253
|
-
],
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
'@hero-design/no-deprecated-component-prop-value': [
|
|
257
|
-
'error',
|
|
24
|
+
// Rules for the next major version
|
|
25
|
+
'@hero-design/no-deprecated-component-prop-next': [
|
|
26
|
+
'warn',
|
|
258
27
|
{
|
|
259
28
|
package: '@hero-design/rn',
|
|
260
29
|
components: [
|
|
30
|
+
{ name: 'Tag', props: ['variant'] },
|
|
31
|
+
{ name: 'FAB.ActionGroup', props: ['headerTitle'] },
|
|
32
|
+
{ name: 'Checkbox', props: ['withBorder'] },
|
|
261
33
|
{
|
|
262
|
-
name: '
|
|
263
|
-
props: [
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
name: 'Button',
|
|
267
|
-
props: [{ name: 'variant', values: ['basic-transparent'] }],
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
name: 'Button.Icon',
|
|
271
|
-
props: [
|
|
272
|
-
{
|
|
273
|
-
name: 'icon',
|
|
274
|
-
values: [
|
|
275
|
-
'carat-down-small',
|
|
276
|
-
'carat-down',
|
|
277
|
-
'carat-left-small',
|
|
278
|
-
'carat-left',
|
|
279
|
-
'carat-right-small',
|
|
280
|
-
'carat-right',
|
|
281
|
-
'carat-up-small',
|
|
282
|
-
'carat-up',
|
|
283
|
-
],
|
|
284
|
-
},
|
|
285
|
-
],
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
name: 'Button.Utility',
|
|
289
|
-
props: [
|
|
290
|
-
{
|
|
291
|
-
name: 'icon',
|
|
292
|
-
values: [
|
|
293
|
-
'carat-down-small',
|
|
294
|
-
'carat-down',
|
|
295
|
-
'carat-left-small',
|
|
296
|
-
'carat-left',
|
|
297
|
-
'carat-right-small',
|
|
298
|
-
'carat-right',
|
|
299
|
-
'carat-up-small',
|
|
300
|
-
'carat-up',
|
|
301
|
-
],
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
{
|
|
306
|
-
name: 'Icon',
|
|
307
|
-
props: [
|
|
308
|
-
{
|
|
309
|
-
name: 'icon',
|
|
310
|
-
values: [
|
|
311
|
-
'carat-down-small',
|
|
312
|
-
'carat-down',
|
|
313
|
-
'carat-left-small',
|
|
314
|
-
'carat-left',
|
|
315
|
-
'carat-right-small',
|
|
316
|
-
'carat-right',
|
|
317
|
-
'carat-up-small',
|
|
318
|
-
'carat-up',
|
|
319
|
-
],
|
|
320
|
-
},
|
|
321
|
-
],
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
name: 'Box',
|
|
325
|
-
props: deprecatedBoxPropValues,
|
|
34
|
+
name: 'SectionHeading',
|
|
35
|
+
props: ['fontSize', 'fontWeight'],
|
|
326
36
|
},
|
|
327
37
|
],
|
|
328
38
|
},
|
|
@@ -408,5 +118,12 @@ module.exports = {
|
|
|
408
118
|
],
|
|
409
119
|
},
|
|
410
120
|
},
|
|
121
|
+
recommendedReact: {
|
|
122
|
+
plugins: ['@hero-design'],
|
|
123
|
+
rules: {
|
|
124
|
+
'@hero-design/no-direct-color-palette-access': 'error',
|
|
125
|
+
'@hero-design/react-no-text-outside-typography': 'warn',
|
|
126
|
+
}
|
|
127
|
+
}
|
|
411
128
|
},
|
|
412
129
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Disallow the use of comments that include @snowflake-guard/',
|
|
7
|
+
},
|
|
8
|
+
messages: {
|
|
9
|
+
noSnowflakeGuard:
|
|
10
|
+
'Comments including @snowflake-guard/ are not allowed. Please contact Andromeda team for the approval.',
|
|
11
|
+
},
|
|
12
|
+
schema: [],
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
return {
|
|
16
|
+
Program() {
|
|
17
|
+
const sourceCode = context.getSourceCode();
|
|
18
|
+
const comments = sourceCode.getAllComments();
|
|
19
|
+
|
|
20
|
+
comments.forEach((comment) => {
|
|
21
|
+
// Check if the comment includes @snowflake-guard/
|
|
22
|
+
if (comment.value.includes('@snowflake-guard/')) {
|
|
23
|
+
context.report({
|
|
24
|
+
node: comment,
|
|
25
|
+
messageId: 'noSnowflakeGuard',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
+
const { findIndentifierPath } = require('../utils');
|
|
8
|
+
|
|
7
9
|
//------------------------------------------------------------------------------
|
|
8
10
|
// Rule Definition
|
|
9
11
|
//------------------------------------------------------------------------------
|
|
@@ -46,27 +48,13 @@ module.exports = {
|
|
|
46
48
|
const oldKeys = context.options[0].keys.map((k) => k.old);
|
|
47
49
|
const newKeys = context.options[0].keys.map((k) => k.new);
|
|
48
50
|
|
|
49
|
-
const findPath = (node, identifiers) => {
|
|
50
|
-
const property = node.property;
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
property != null &&
|
|
54
|
-
property.type === 'Identifier' &&
|
|
55
|
-
property.name != null
|
|
56
|
-
) {
|
|
57
|
-
return findPath(node.parent, [...identifiers, property.name]);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return identifiers.join('.');
|
|
61
|
-
};
|
|
62
|
-
|
|
63
51
|
return {
|
|
64
52
|
MemberExpression(node) {
|
|
65
53
|
const object = node.object;
|
|
66
54
|
if (object == null) return;
|
|
67
55
|
|
|
68
56
|
if (object.type === 'Identifier' && object.name === 'theme') {
|
|
69
|
-
const path =
|
|
57
|
+
const path = findIndentifierPath(node, []);
|
|
70
58
|
const deprecatedKeyIndex = oldKeys.findIndex((k) => k === path);
|
|
71
59
|
|
|
72
60
|
if (deprecatedKeyIndex >= 0) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Disallow direct access of color palette.
|
|
3
|
+
*/
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { findIndentifierPath } = require('../utils');
|
|
7
|
+
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
// Rule Definition
|
|
10
|
+
//------------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow direct access of color palette.',
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
invalidColorAccess: 'Unexpected direct access of palette colors, use semantic colors instead.',
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
MemberExpression(node) {
|
|
28
|
+
const object = node.object;
|
|
29
|
+
if (object == null) return;
|
|
30
|
+
|
|
31
|
+
// For example: theme.colors.pallete.redLight30
|
|
32
|
+
if (object.type === 'Identifier' && object.name === 'theme') {
|
|
33
|
+
const path = findIndentifierPath(node, []);
|
|
34
|
+
|
|
35
|
+
if (path.includes('colors.palette')) {
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: 'invalidColorAccess',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// For example: colors.pallete.redLight30
|
|
43
|
+
else if (object.type === 'Identifier' && object.name === 'colors') {
|
|
44
|
+
const path = findIndentifierPath(node, []);
|
|
45
|
+
|
|
46
|
+
if (path.includes('palette')) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: 'invalidColorAccess',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -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/lib/utils.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const findIndentifierPath = (node, identifiers) => {
|
|
2
|
+
const property = node.property;
|
|
3
|
+
|
|
4
|
+
if (
|
|
5
|
+
property != null &&
|
|
6
|
+
property.type === 'Identifier' &&
|
|
7
|
+
property.name != null
|
|
8
|
+
) {
|
|
9
|
+
return findIndentifierPath(node.parent, [...identifiers, property.name]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return identifiers.join('.');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
findIndentifierPath,
|
|
17
|
+
}
|