@af-mobile/eslint-plugin 2.0.2 → 2.0.4
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/index.js +1 -1
- package/package.json +1 -1
- package/rules/no-recipe-break.js +3 -3
- package/rules/token-whitelist.js +14 -8
- package/rules/wc-aria-required.js +1 -1
- package/utils/aria-requirements.json +14 -0
- package/utils/helpers.js +29 -0
- package/utils/whitelist-v1.json +68 -30
package/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import wcBlockStates from './rules/wc-block-states.js';
|
|
|
23
23
|
import wcBlockVariantEnum from './rules/wc-block-variant-enum.js';
|
|
24
24
|
|
|
25
25
|
const plugin = {
|
|
26
|
-
meta: { name: 'eslint-plugin-af-mobile', version: '2.0.
|
|
26
|
+
meta: { name: 'eslint-plugin-af-mobile', version: '2.0.4' },
|
|
27
27
|
rules: {
|
|
28
28
|
// L1(2 条,全部 error;tokens-css-locked 由 CODEOWNERS + 分支保护,非 ESLint 规则)
|
|
29
29
|
'no-token-modification': noTokenModification,
|
package/package.json
CHANGED
package/rules/no-recipe-break.js
CHANGED
|
@@ -18,7 +18,7 @@ export default {
|
|
|
18
18
|
messages: {
|
|
19
19
|
cellFlex: ".cell has built-in display:flex, adding 'f'/'fc' may break layout",
|
|
20
20
|
btnColor: ".btn background uses --c-brand, text color must keep --c-onbrand contrast. Use .btn-ghost instead for colored text",
|
|
21
|
-
inputFont: ".input font-size must be 16px (--t-
|
|
21
|
+
inputFont: "form control (.input/.textarea/.search-input) font-size must be 16px (--t-input) to prevent iOS focus zoom. Put help text in <label> or .form-err instead",
|
|
22
22
|
btnBg: ".btn background is --c-brand with white text; overriding background with 'bg-card'/'bg-muted' breaks contrast. Use .btn-ghost instead",
|
|
23
23
|
},
|
|
24
24
|
},
|
|
@@ -41,8 +41,8 @@ export default {
|
|
|
41
41
|
if (set.has(a)) { context.report({ node, messageId: 'btnBg' }); break; }
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
// c:
|
|
45
|
-
if (set.has('input')) {
|
|
44
|
+
// c: 表单控件 + t-sm/t-xs(.input/.textarea/.search-input 均为 --t-input 16px 基准)
|
|
45
|
+
if (set.has('input') || set.has('textarea') || set.has('search-input')) {
|
|
46
46
|
for (const a of SMALL_FONT) {
|
|
47
47
|
if (set.has(a)) { context.report({ node, messageId: 'inputFont' }); break; }
|
|
48
48
|
}
|
package/rules/token-whitelist.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// L2-1 af-mobile/token-whitelist(error)
|
|
2
2
|
// 检测:class="" 中的 class / 自定义元素 tagName → 与 whitelist + extraClass/extraComponents 对比
|
|
3
|
-
|
|
3
|
+
// 模板插值 class(btn-${type})无法静态解析 → 单独报 interpolatedClass(伪报为"不在白名单"会误导修法)
|
|
4
|
+
import { ALL_CLASSES, ALL_COMPONENTS, extractAllClassLists, extractCustomElements, suggestNearest } from '../utils/helpers.js';
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
meta: {
|
|
@@ -15,28 +16,33 @@ export default {
|
|
|
15
16
|
additionalProperties: false,
|
|
16
17
|
}],
|
|
17
18
|
messages: {
|
|
18
|
-
unknownClass: "Class '{{name}}' not in whitelist. Use recipe/atomic or
|
|
19
|
-
unknownComponent: "Component '{{name}}' not in whitelist.
|
|
19
|
+
unknownClass: "Class '{{name}}' not in whitelist.{{suggest}} Use recipe/atomic, or paste into eslint.config.js: 'af-mobile/token-whitelist': ['error', { extraClass: ['{{name}}'] }]",
|
|
20
|
+
unknownComponent: "Component '{{name}}' not in whitelist.{{suggest}} Paste into eslint.config.js: 'af-mobile/token-whitelist': ['error', { extraComponents: ['{{name}}'] }]",
|
|
21
|
+
interpolatedClass: "Class '{{name}}' contains template interpolation and cannot be checked statically. Use static whitelist classes plus data-* attributes, or a ternary switching between whitelisted classes",
|
|
20
22
|
},
|
|
21
23
|
},
|
|
22
24
|
create(context) {
|
|
23
25
|
const options = context.options[0] || {};
|
|
24
26
|
const allowClass = new Set([...ALL_CLASSES, ...(options.extraClass || [])]);
|
|
25
27
|
const allowComp = new Set([...ALL_COMPONENTS, ...(options.extraComponents || [])]);
|
|
28
|
+
const extraClasses = [...allowClass];
|
|
29
|
+
const extraComps = [...allowComp];
|
|
26
30
|
|
|
27
31
|
function checkString(str, node) {
|
|
28
32
|
// class 检查
|
|
29
33
|
for (const { classes } of extractAllClassLists(str)) {
|
|
30
34
|
for (const cls of classes) {
|
|
31
|
-
if (
|
|
32
|
-
context.report({ node, messageId: '
|
|
35
|
+
if (cls.includes('${')) {
|
|
36
|
+
context.report({ node, messageId: 'interpolatedClass', data: { name: cls } });
|
|
37
|
+
} else if (!allowClass.has(cls)) {
|
|
38
|
+
context.report({ node, messageId: 'unknownClass', data: { name: cls, suggest: suggestNearest(cls, extraClasses) } });
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
41
|
}
|
|
36
42
|
// 自定义元素检查
|
|
37
43
|
for (const tag of extractCustomElements(str)) {
|
|
38
44
|
if (!allowComp.has(tag)) {
|
|
39
|
-
context.report({ node, messageId: 'unknownComponent', data: { name: tag } });
|
|
45
|
+
context.report({ node, messageId: 'unknownComponent', data: { name: tag, suggest: suggestNearest(tag, extraComps) } });
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
@@ -56,7 +62,7 @@ export default {
|
|
|
56
62
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
57
63
|
for (const cls of arg.value.split(/\s+/).filter(Boolean)) {
|
|
58
64
|
if (!allowClass.has(cls)) {
|
|
59
|
-
context.report({ node: arg, messageId: 'unknownClass', data: { name: cls } });
|
|
65
|
+
context.report({ node: arg, messageId: 'unknownClass', data: { name: cls, suggest: suggestNearest(cls, extraClasses) } });
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
}
|
|
@@ -65,7 +71,7 @@ export default {
|
|
|
65
71
|
if (el?.type === 'Literal' && typeof el.value === 'string') {
|
|
66
72
|
for (const cls of el.value.split(/\s+/).filter(Boolean)) {
|
|
67
73
|
if (!allowClass.has(cls)) {
|
|
68
|
-
context.report({ node: el, messageId: 'unknownClass', data: { name: cls } });
|
|
74
|
+
context.report({ node: el, messageId: 'unknownClass', data: { name: cls, suggest: suggestNearest(cls, extraClasses) } });
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
}
|
|
@@ -22,7 +22,7 @@ export default {
|
|
|
22
22
|
},
|
|
23
23
|
create(context) {
|
|
24
24
|
const filename = context.filename || context.getFilename();
|
|
25
|
-
if (!/src[\\/](?:charts[\\/])?components[\\/].*\.js$/.test(filename)) return {};
|
|
25
|
+
if (!/src[\\/](?:(?:charts|chat)[\\/])?components[\\/].*\.js$/.test(filename)) return {};
|
|
26
26
|
|
|
27
27
|
// 从文件名提取组件名(af-xxx.js → af-xxx)
|
|
28
28
|
const m = filename.match(/(af-[a-z-]+)\.js$/);
|
|
@@ -39,6 +39,15 @@
|
|
|
39
39
|
"role": "listbox",
|
|
40
40
|
"description": "选择器列需要 role=listbox"
|
|
41
41
|
},
|
|
42
|
+
"af-number-keyboard": {
|
|
43
|
+
"role": "dialog",
|
|
44
|
+
"ariaLabel": true,
|
|
45
|
+
"description": "数字键盘弹层需要 role=dialog 和 aria-label;按键为原生 button 天然支持键盘操作"
|
|
46
|
+
},
|
|
47
|
+
"af-password-input": {
|
|
48
|
+
"ariaLabel": true,
|
|
49
|
+
"description": "密码格子容器需要 aria-label(含输入进度播报)"
|
|
50
|
+
},
|
|
42
51
|
"af-img": {
|
|
43
52
|
"description": "图片组件无强制 ARIA 要求"
|
|
44
53
|
},
|
|
@@ -83,5 +92,10 @@
|
|
|
83
92
|
"role": "img",
|
|
84
93
|
"ariaLabel": true,
|
|
85
94
|
"description": "漏斗图 svg 需要 role=img 和 aria-label 摘要(全量数据由 sr-table 提供)"
|
|
95
|
+
},
|
|
96
|
+
"af-chat": {
|
|
97
|
+
"role": "log",
|
|
98
|
+
"ariaLive": true,
|
|
99
|
+
"description": "对话记录容器需要 role=log 和 aria-live=polite(流式文本 aria-hidden,完成时 sr-only 一次性播报)"
|
|
86
100
|
}
|
|
87
101
|
}
|
package/utils/helpers.js
CHANGED
|
@@ -53,3 +53,32 @@ export function extractCustomElements(str) {
|
|
|
53
53
|
while ((m = re.exec(str))) results.push(m[1]);
|
|
54
54
|
return results;
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
// 编辑距离(Levenshtein):最近邻建议用,候选集小(<200)无需提前剪枝
|
|
58
|
+
function levenshtein(a, b) {
|
|
59
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
60
|
+
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
|
|
61
|
+
for (let i = 1; i <= a.length; i++) {
|
|
62
|
+
for (let j = 1; j <= b.length; j++) {
|
|
63
|
+
dp[i][j] = Math.min(
|
|
64
|
+
dp[i - 1][j] + 1,
|
|
65
|
+
dp[i][j - 1] + 1,
|
|
66
|
+
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return dp[a.length][b.length];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 最近邻建议:返回 " Did you mean 'xxx'?" 或 ''(无足够接近的候选)
|
|
74
|
+
// 阈值随长度放宽:≤3 字符允许 1 距离,更长允许 2
|
|
75
|
+
export function suggestNearest(name, candidates) {
|
|
76
|
+
const max = name.length <= 3 ? 1 : 2;
|
|
77
|
+
let best = null, bestDist = max + 1;
|
|
78
|
+
for (const c of candidates) {
|
|
79
|
+
if (Math.abs(c.length - name.length) > max) continue;
|
|
80
|
+
const d = levenshtein(name, c);
|
|
81
|
+
if (d < bestDist) { best = c; bestDist = d; }
|
|
82
|
+
}
|
|
83
|
+
return best ? ` Did you mean '${best}'?` : '';
|
|
84
|
+
}
|
package/utils/whitelist-v1.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "v1",
|
|
3
|
-
"af-mobileVersion": "1.
|
|
3
|
+
"af-mobileVersion": "1.5.2",
|
|
4
4
|
"classes": {
|
|
5
5
|
"recipe": [
|
|
6
6
|
"actions",
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"btn-danger",
|
|
13
13
|
"btn-ghost",
|
|
14
14
|
"btn-lg",
|
|
15
|
+
"btn-mini",
|
|
16
|
+
"btn-plain",
|
|
17
|
+
"btn-round",
|
|
15
18
|
"btn-sm",
|
|
16
19
|
"btn-success",
|
|
17
20
|
"caption",
|
|
@@ -20,10 +23,11 @@
|
|
|
20
23
|
"center",
|
|
21
24
|
"checkbox",
|
|
22
25
|
"checkbox-sm",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
26
|
+
"clp",
|
|
27
|
+
"clp-ct",
|
|
28
|
+
"clp-sum",
|
|
29
|
+
"cob",
|
|
30
|
+
"cob-fx",
|
|
27
31
|
"display",
|
|
28
32
|
"divider",
|
|
29
33
|
"empty",
|
|
@@ -34,17 +38,18 @@
|
|
|
34
38
|
"hero",
|
|
35
39
|
"input",
|
|
36
40
|
"input-bar",
|
|
41
|
+
"input-bar-fx",
|
|
37
42
|
"input-err",
|
|
38
43
|
"label",
|
|
39
44
|
"list",
|
|
40
45
|
"list-item",
|
|
41
|
-
"list-item-
|
|
46
|
+
"list-item-cp",
|
|
42
47
|
"meta",
|
|
43
48
|
"navbar",
|
|
44
49
|
"navbar-fixed",
|
|
45
50
|
"notice",
|
|
46
|
-
"notice-
|
|
47
|
-
"notice-
|
|
51
|
+
"notice-scr",
|
|
52
|
+
"notice-tx",
|
|
48
53
|
"page",
|
|
49
54
|
"page-col",
|
|
50
55
|
"price",
|
|
@@ -58,30 +63,32 @@
|
|
|
58
63
|
"radio-sm",
|
|
59
64
|
"rate",
|
|
60
65
|
"rate-lg",
|
|
61
|
-
"rate-
|
|
66
|
+
"rate-ro",
|
|
62
67
|
"rate-sm",
|
|
63
68
|
"rate-star",
|
|
69
|
+
"safe-bottom",
|
|
70
|
+
"safe-top",
|
|
71
|
+
"sb-clear",
|
|
72
|
+
"sb-icon",
|
|
73
|
+
"sb-wrap",
|
|
64
74
|
"scroll-y",
|
|
65
|
-
"search-bar-clear",
|
|
66
|
-
"search-bar-icon",
|
|
67
|
-
"search-bar-wrap",
|
|
68
75
|
"search-input",
|
|
69
76
|
"section",
|
|
70
|
-
"section-
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
77
|
+
"section-tt",
|
|
78
|
+
"seg",
|
|
79
|
+
"seg-blk",
|
|
80
|
+
"seg-it",
|
|
74
81
|
"sheet",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
82
|
+
"sk",
|
|
83
|
+
"sk-blk",
|
|
84
|
+
"sk-blk-h-md",
|
|
85
|
+
"sk-blk-h-sm",
|
|
86
|
+
"sk-cir",
|
|
87
|
+
"sk-ln",
|
|
88
|
+
"sk-pg",
|
|
89
|
+
"sk-w-40",
|
|
90
|
+
"sk-w-60",
|
|
91
|
+
"sk-w-80",
|
|
85
92
|
"spinner",
|
|
86
93
|
"spinner-lg",
|
|
87
94
|
"spinner-sm",
|
|
@@ -94,16 +101,19 @@
|
|
|
94
101
|
"steps",
|
|
95
102
|
"subtitle",
|
|
96
103
|
"switch",
|
|
97
|
-
"switch-
|
|
104
|
+
"switch-ldg",
|
|
98
105
|
"switch-on",
|
|
99
106
|
"switch-sm",
|
|
100
|
-
"switch-
|
|
107
|
+
"switch-th",
|
|
101
108
|
"tab-item",
|
|
102
109
|
"tabbar",
|
|
103
110
|
"tabbar-fixed",
|
|
104
111
|
"tag",
|
|
105
112
|
"tag-danger",
|
|
113
|
+
"tag-lg",
|
|
114
|
+
"tag-md",
|
|
106
115
|
"tag-ok",
|
|
116
|
+
"tag-plain",
|
|
107
117
|
"tag-warn",
|
|
108
118
|
"textarea",
|
|
109
119
|
"thumb",
|
|
@@ -112,8 +122,8 @@
|
|
|
112
122
|
"toast-error",
|
|
113
123
|
"toast-success",
|
|
114
124
|
"toast-warning",
|
|
115
|
-
"upload-
|
|
116
|
-
"upload-
|
|
125
|
+
"upload-gd",
|
|
126
|
+
"upload-tg"
|
|
117
127
|
],
|
|
118
128
|
"atomic": [
|
|
119
129
|
"aic",
|
|
@@ -121,6 +131,7 @@
|
|
|
121
131
|
"bg-card",
|
|
122
132
|
"bg-muted",
|
|
123
133
|
"ellipsis",
|
|
134
|
+
"ellipsis-2",
|
|
124
135
|
"f",
|
|
125
136
|
"fc",
|
|
126
137
|
"fi",
|
|
@@ -130,17 +141,21 @@
|
|
|
130
141
|
"g-2",
|
|
131
142
|
"g-3",
|
|
132
143
|
"g-4",
|
|
144
|
+
"hl-b",
|
|
145
|
+
"hl-t",
|
|
133
146
|
"jcc",
|
|
134
147
|
"jce",
|
|
135
148
|
"jcsb",
|
|
136
149
|
"lh-normal",
|
|
137
150
|
"lh-tight",
|
|
151
|
+
"list",
|
|
138
152
|
"m-0",
|
|
139
153
|
"m-1",
|
|
140
154
|
"m-2",
|
|
141
155
|
"m-3",
|
|
142
156
|
"m-4",
|
|
143
157
|
"m-5",
|
|
158
|
+
"navbar",
|
|
144
159
|
"p-0",
|
|
145
160
|
"p-1",
|
|
146
161
|
"p-10",
|
|
@@ -191,6 +206,7 @@
|
|
|
191
206
|
"af-chart-line",
|
|
192
207
|
"af-chart-pie",
|
|
193
208
|
"af-chart-radar",
|
|
209
|
+
"af-chat",
|
|
194
210
|
"af-countdown",
|
|
195
211
|
"af-dialog",
|
|
196
212
|
"af-dropdown",
|
|
@@ -199,6 +215,8 @@
|
|
|
199
215
|
"af-list",
|
|
200
216
|
"af-navbar",
|
|
201
217
|
"af-notice-bar",
|
|
218
|
+
"af-number-keyboard",
|
|
219
|
+
"af-password-input",
|
|
202
220
|
"af-picker",
|
|
203
221
|
"af-progress",
|
|
204
222
|
"af-pull-refresh",
|
|
@@ -223,9 +241,18 @@
|
|
|
223
241
|
"--c-brand-strong",
|
|
224
242
|
"--c-card",
|
|
225
243
|
"--c-danger",
|
|
244
|
+
"--c-gray-1",
|
|
245
|
+
"--c-gray-2",
|
|
246
|
+
"--c-gray-3",
|
|
247
|
+
"--c-gray-4",
|
|
248
|
+
"--c-gray-5",
|
|
249
|
+
"--c-gray-6",
|
|
250
|
+
"--c-gray-7",
|
|
251
|
+
"--c-gray-8",
|
|
226
252
|
"--c-muted",
|
|
227
253
|
"--c-muted-bg",
|
|
228
254
|
"--c-onbrand",
|
|
255
|
+
"--c-onwarn",
|
|
229
256
|
"--c-success",
|
|
230
257
|
"--c-text",
|
|
231
258
|
"--c-warn",
|
|
@@ -251,9 +278,18 @@
|
|
|
251
278
|
"--palette-card",
|
|
252
279
|
"--palette-color-scheme",
|
|
253
280
|
"--palette-danger",
|
|
281
|
+
"--palette-gray-1",
|
|
282
|
+
"--palette-gray-2",
|
|
283
|
+
"--palette-gray-3",
|
|
284
|
+
"--palette-gray-4",
|
|
285
|
+
"--palette-gray-5",
|
|
286
|
+
"--palette-gray-6",
|
|
287
|
+
"--palette-gray-7",
|
|
288
|
+
"--palette-gray-8",
|
|
254
289
|
"--palette-muted",
|
|
255
290
|
"--palette-muted-bg",
|
|
256
291
|
"--palette-onbrand",
|
|
292
|
+
"--palette-onwarn",
|
|
257
293
|
"--palette-shadow-lg",
|
|
258
294
|
"--palette-shadow-md",
|
|
259
295
|
"--palette-shadow-sm",
|
|
@@ -279,11 +315,13 @@
|
|
|
279
315
|
"--shadow-md",
|
|
280
316
|
"--shadow-sm",
|
|
281
317
|
"--t-display",
|
|
318
|
+
"--t-input",
|
|
282
319
|
"--t-lg",
|
|
283
320
|
"--t-md",
|
|
284
321
|
"--t-sm",
|
|
285
322
|
"--t-xl",
|
|
286
323
|
"--t-xs",
|
|
324
|
+
"--tabbar-h",
|
|
287
325
|
"--z-base",
|
|
288
326
|
"--z-dropdown",
|
|
289
327
|
"--z-modal",
|