@af-mobile/eslint-plugin 2.0.1 → 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 +24 -24
- package/package.json +3 -3
- package/rules/atomic-duplicate.js +4 -4
- package/rules/no-arbitrary-value.js +3 -3
- package/rules/no-inline-style.js +1 -1
- package/rules/no-recipe-break.js +14 -7
- package/rules/no-register-all.js +7 -5
- package/rules/no-tailwind-syntax.js +1 -1
- package/rules/no-token-modification.js +1 -1
- package/rules/no-variant-conflict.js +11 -9
- package/rules/prefer-component.js +1 -1
- package/rules/token-whitelist.js +15 -9
- package/rules/wc-aria-required.js +2 -2
- package/rules/wc-block-no-internal-ref.js +3 -2
- package/rules/wc-block-props-count.js +1 -1
- package/rules/wc-block-states.js +1 -1
- package/rules/wc-block-variant-enum.js +10 -1
- package/rules/wc-cleanup.js +1 -1
- package/rules/wc-event-naming.js +10 -5
- package/rules/wc-light-no-style.js +1 -1
- package/rules/wc-part-naming.js +1 -1
- package/rules/wc-shadow-use-token.js +1 -1
- package/utils/aria-requirements.json +14 -0
- package/utils/helpers.js +30 -1
- package/utils/whitelist-v1.json +98 -29
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// af-mobile UI —— eslint-plugin-af-mobile 入口(ESLint 9 flat config 兼容)
|
|
2
2
|
// 20 条规则:L1(2) + L2(7) + L3(6) + L3.5(5)
|
|
3
3
|
// (definePage 消费端 7 条规则已随 definePage 全局单例移除)
|
|
4
4
|
import noTokenModification from './rules/no-token-modification.js';
|
|
@@ -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-
|
|
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,
|
|
@@ -57,37 +57,37 @@ const plugin = {
|
|
|
57
57
|
// recommended 配置:按设计文档 L4 §3.2-3.4 的 severity 启用
|
|
58
58
|
Object.assign(plugin.configs, {
|
|
59
59
|
recommended: {
|
|
60
|
-
plugins: {
|
|
60
|
+
plugins: { 'af-mobile': plugin },
|
|
61
61
|
rules: {
|
|
62
62
|
// L1 error
|
|
63
|
-
'
|
|
64
|
-
'
|
|
63
|
+
'af-mobile/no-token-modification': 'error',
|
|
64
|
+
'af-mobile/no-inline-style': 'error',
|
|
65
65
|
// L2 error
|
|
66
66
|
// extraComponents: af-data 属 L3.5 Block 层数据源元素(非 L3 注册组件,不在 whitelist-v1.json),
|
|
67
67
|
// 但 AI 生成的页面代码(createPage/:bind 体系)需要能使用 <af-data>,故在规则级放行
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
68
|
+
'af-mobile/token-whitelist': ['error', { extraComponents: ['af-data'] }],
|
|
69
|
+
'af-mobile/no-recipe-break': 'error',
|
|
70
|
+
'af-mobile/no-arbitrary-value': 'error',
|
|
71
|
+
'af-mobile/no-tailwind-syntax': 'error',
|
|
72
72
|
// L2 warn
|
|
73
|
-
'
|
|
74
|
-
'
|
|
75
|
-
'
|
|
73
|
+
'af-mobile/no-variant-conflict': 'warn',
|
|
74
|
+
'af-mobile/prefer-component': 'warn',
|
|
75
|
+
'af-mobile/atomic-duplicate': 'warn',
|
|
76
76
|
// L3 error
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
77
|
+
'af-mobile/wc-light-no-style': 'error',
|
|
78
|
+
'af-mobile/wc-shadow-use-token': 'error',
|
|
79
|
+
'af-mobile/wc-event-naming': 'error',
|
|
80
|
+
'af-mobile/wc-aria-required': 'error',
|
|
81
81
|
// L3 warn
|
|
82
|
-
'
|
|
83
|
-
'
|
|
82
|
+
'af-mobile/wc-part-naming': 'warn',
|
|
83
|
+
'af-mobile/wc-cleanup': 'warn',
|
|
84
84
|
// L3.5 error(Block 层约束)
|
|
85
|
-
'
|
|
86
|
-
'
|
|
87
|
-
'
|
|
88
|
-
// L3.5
|
|
89
|
-
'
|
|
90
|
-
'
|
|
85
|
+
'af-mobile/wc-block-no-internal-ref': 'error',
|
|
86
|
+
'af-mobile/wc-block-props-count': 'error',
|
|
87
|
+
'af-mobile/wc-block-states': 'error',
|
|
88
|
+
// L3.5 error(按需引入铁律:registerAll 全量注册 = 全局引入,一律禁止)
|
|
89
|
+
'af-mobile/no-register-all': 'error',
|
|
90
|
+
'af-mobile/wc-block-variant-enum': 'warn',
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
93
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@af-mobile/eslint-plugin",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.4",
|
|
4
|
+
"description": "af-mobile UI ESLint plugin — 27 rules (L1/L2/L3/L3.5) enforcing design-system + Web Component constraints",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"module": "./index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"eslint-plugin",
|
|
26
26
|
"web-components",
|
|
27
27
|
"design-system",
|
|
28
|
-
"
|
|
28
|
+
"af-mobile"
|
|
29
29
|
],
|
|
30
30
|
"license": "MIT"
|
|
31
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L2-7
|
|
1
|
+
// L2-7 af-mobile/atomic-duplicate(warn,可自动修)
|
|
2
2
|
// 检测:同一 class 属性内出现两个同属性原子(如 p-4 p-2),保留最后一个
|
|
3
3
|
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
4
|
|
|
@@ -34,15 +34,15 @@ export default {
|
|
|
34
34
|
messageId: 'duplicate',
|
|
35
35
|
data: { prop: PROP_LABELS[prefix] || prefix, a: prev.cls, b: cls },
|
|
36
36
|
fix(fixer) {
|
|
37
|
+
// 删除被覆盖的 class + 其后的空格(保留的类在后面,必有空格可吃)
|
|
37
38
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
38
39
|
const text = sourceCode.getText(node);
|
|
39
40
|
const idx = text.indexOf(raw);
|
|
40
41
|
if (idx < 0) return null;
|
|
41
42
|
const classIdx = idx + raw.indexOf(prev.cls);
|
|
42
43
|
const before = text.slice(0, classIdx);
|
|
43
|
-
const after = text.slice(classIdx + prev.cls.length);
|
|
44
|
-
|
|
45
|
-
return fixer.replaceText(node, trimmedBefore + after);
|
|
44
|
+
const after = text.slice(classIdx + prev.cls.length).replace(/^\s+/, '');
|
|
45
|
+
return fixer.replaceText(node, before + after);
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// L2-4
|
|
2
|
-
// 检测:class 含 [xxx] 任意值语法 / 档位越界(如 p-
|
|
1
|
+
// L2-4 af-mobile/no-arbitrary-value(error,部分可自动修)
|
|
2
|
+
// 检测:class 含 [xxx] 任意值语法 / 档位越界(如 p-9)
|
|
3
3
|
import { extractAllClassLists, ALL_CLASSES } from '../utils/helpers.js';
|
|
4
4
|
|
|
5
5
|
// 合法档位
|
|
6
6
|
const RANGES = {
|
|
7
|
-
'p': ['0', '1', '2', '3', '4', '5', '6', '8', '10'],
|
|
7
|
+
'p': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '10'],
|
|
8
8
|
'm': ['0', '1', '2', '3', '4', '5', '6'],
|
|
9
9
|
't': ['xs', 'sm', 'md', 'lg', 'xl'],
|
|
10
10
|
'r': ['s', 'm', 'l', 'f'],
|
package/rules/no-inline-style.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L1-2
|
|
1
|
+
// L1-2 af-mobile/no-inline-style(error,部分可自动修)
|
|
2
2
|
// 检测:style="" 中设置 forbiddenInlineStyle 列表内的属性
|
|
3
3
|
// 例外:display/transform/z-index/width/height 等布局属性放行;skeleton class 元素内 width/height 放行
|
|
4
4
|
// 适用:HTML/JS 文件中所有字符串字面量(含 template literal),覆盖组件 innerHTML 赋值场景
|
package/rules/no-recipe-break.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
// L2-2
|
|
2
|
-
// a: .cell + f/fc 原子 → 布局冲突
|
|
1
|
+
// L2-2 af-mobile/no-recipe-break(error,4 子规则)
|
|
2
|
+
// a: .cell + f/fi/fc 原子 → 布局冲突
|
|
3
3
|
// b: .btn(非 .btn-ghost)+ text-brand/text-danger/text-success → 对比度问题
|
|
4
4
|
// c: .input + t-sm/t-xs → iOS 焦点缩放
|
|
5
|
+
// d: .btn(非 .btn-ghost)+ bg-card/bg-muted → 背景覆盖破坏 onbrand 对比度
|
|
5
6
|
import { extractAllClassLists } from '../utils/helpers.js';
|
|
6
7
|
|
|
7
|
-
const FLEX_ATOMS = new Set(['f', 'fc']);
|
|
8
|
+
const FLEX_ATOMS = new Set(['f', 'fi', 'fc']);
|
|
8
9
|
const COLORED_TEXT = new Set(['text-brand', 'text-danger', 'text-success']);
|
|
9
10
|
const SMALL_FONT = new Set(['t-sm', 't-xs']);
|
|
11
|
+
const BROKEN_BG = new Set(['bg-card', 'bg-muted']);
|
|
10
12
|
|
|
11
13
|
export default {
|
|
12
14
|
meta: {
|
|
@@ -16,13 +18,14 @@ export default {
|
|
|
16
18
|
messages: {
|
|
17
19
|
cellFlex: ".cell has built-in display:flex, adding 'f'/'fc' may break layout",
|
|
18
20
|
btnColor: ".btn background uses --c-brand, text color must keep --c-onbrand contrast. Use .btn-ghost instead for colored text",
|
|
19
|
-
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
|
+
btnBg: ".btn background is --c-brand with white text; overriding background with 'bg-card'/'bg-muted' breaks contrast. Use .btn-ghost instead",
|
|
20
23
|
},
|
|
21
24
|
},
|
|
22
25
|
create(context) {
|
|
23
26
|
function checkClasses(classes, node) {
|
|
24
27
|
const set = new Set(classes);
|
|
25
|
-
// a: .cell + f/fc
|
|
28
|
+
// a: .cell + f/fi/fc
|
|
26
29
|
if (set.has('cell')) {
|
|
27
30
|
for (const a of FLEX_ATOMS) {
|
|
28
31
|
if (set.has(a)) { context.report({ node, messageId: 'cellFlex' }); break; }
|
|
@@ -33,9 +36,13 @@ export default {
|
|
|
33
36
|
for (const a of COLORED_TEXT) {
|
|
34
37
|
if (set.has(a)) { context.report({ node, messageId: 'btnColor' }); break; }
|
|
35
38
|
}
|
|
39
|
+
// d: 背景覆盖同样破坏 onbrand 对比度
|
|
40
|
+
for (const a of BROKEN_BG) {
|
|
41
|
+
if (set.has(a)) { context.report({ node, messageId: 'btnBg' }); break; }
|
|
42
|
+
}
|
|
36
43
|
}
|
|
37
|
-
// c:
|
|
38
|
-
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')) {
|
|
39
46
|
for (const a of SMALL_FONT) {
|
|
40
47
|
if (set.has(a)) { context.report({ node, messageId: 'inputFont' }); break; }
|
|
41
48
|
}
|
package/rules/no-register-all.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// L3
|
|
2
|
-
// 检测:registerAll()
|
|
1
|
+
// L3 af-mobile/no-register-all(error)
|
|
2
|
+
// 检测:registerAll() 调用(已废弃/已移除,诱导全量加载,失去 Tree Shaking)
|
|
3
3
|
// 正确:register('af-list', 'af-dialog') 显式列名,配合 Tree Shaking 按需打包
|
|
4
4
|
export default {
|
|
5
5
|
meta: {
|
|
@@ -7,13 +7,15 @@ export default {
|
|
|
7
7
|
docs: { description: '禁止 registerAll(),改用 register(...names) 显式列名', fixable: 'ai-rewrite' },
|
|
8
8
|
schema: [],
|
|
9
9
|
messages: {
|
|
10
|
-
registerAll: "registerAll()
|
|
10
|
+
registerAll: "registerAll() 已废弃/已移除(全量注册 = 全局引入),改用 register(...names) 显式列名",
|
|
11
11
|
},
|
|
12
12
|
},
|
|
13
13
|
create(context) {
|
|
14
14
|
const filename = context.filename || context.getFilename();
|
|
15
|
-
//
|
|
16
|
-
|
|
15
|
+
// 非消费端放行:单元测试夹具 / 构建脚本
|
|
16
|
+
// 注意:原正则含 `src[\\/]` 会误伤消费端 src/main.js——消费端恰恰是要约束的对象。
|
|
17
|
+
// 库源码 src/ 不在此 skip 也安全:库源码已移除 registerAll(按需引入铁律)。
|
|
18
|
+
if (/test[\\/]|scripts[\\/]/.test(filename)) return {};
|
|
17
19
|
return {
|
|
18
20
|
CallExpression(node) {
|
|
19
21
|
const callee = node.callee;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L2-3
|
|
1
|
+
// L2-3 af-mobile/no-variant-conflict(warn,可自动修)
|
|
2
2
|
// 检测:同属性互斥变体对同时出现,自动修:保留最后一个
|
|
3
3
|
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
4
|
|
|
@@ -9,7 +9,13 @@ const CONFLICT_GROUPS = [
|
|
|
9
9
|
// 圆角类
|
|
10
10
|
['r-s', 'r-m', 'r-l', 'r-f'],
|
|
11
11
|
// padding 类
|
|
12
|
-
['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-8', 'p-10'],
|
|
12
|
+
['p-0', 'p-1', 'p-2', 'p-3', 'p-4', 'p-5', 'p-6', 'p-7', 'p-8', 'p-10'],
|
|
13
|
+
// display 类(同为 display 属性)
|
|
14
|
+
['f', 'fi', 'fc'],
|
|
15
|
+
// 行高类
|
|
16
|
+
['lh-tight', 'lh-normal'],
|
|
17
|
+
// 背景类
|
|
18
|
+
['bg-brand', 'bg-card', 'bg-muted'],
|
|
13
19
|
];
|
|
14
20
|
|
|
15
21
|
export default {
|
|
@@ -39,19 +45,15 @@ export default {
|
|
|
39
45
|
messageId: 'conflict',
|
|
40
46
|
data: { a: r, b: keep },
|
|
41
47
|
fix(fixer) {
|
|
42
|
-
// 删除 class
|
|
48
|
+
// 删除 class 名 + 其后的一个空格(保留的类在后面,必有空格可吃)
|
|
43
49
|
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
44
50
|
const text = sourceCode.getText(node);
|
|
45
51
|
const idx = text.indexOf(raw);
|
|
46
52
|
if (idx < 0) return null;
|
|
47
53
|
const classIdx = idx + raw.indexOf(r);
|
|
48
|
-
// 删除 " r" 或 "r " 或 "r"
|
|
49
54
|
const before = text.slice(0, classIdx);
|
|
50
|
-
const after = text.slice(classIdx + r.length);
|
|
51
|
-
|
|
52
|
-
const trimmedBefore = before.replace(/\s+$/, '');
|
|
53
|
-
const newText = trimmedBefore + after;
|
|
54
|
-
return fixer.replaceText(node, newText);
|
|
55
|
+
const after = text.slice(classIdx + r.length).replace(/^\s+/, '');
|
|
56
|
+
return fixer.replaceText(node, before + after);
|
|
55
57
|
},
|
|
56
58
|
});
|
|
57
59
|
}
|
package/rules/token-whitelist.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// L2-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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3-5
|
|
1
|
+
// L3-5 af-mobile/wc-aria-required(error)
|
|
2
2
|
// 检测:组件 render 输出的 DOM 缺少必需的 ARIA 角色/属性
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -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$/);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3.5
|
|
1
|
+
// L3.5 af-mobile/wc-block-no-internal-ref(error)
|
|
2
2
|
// 检测:消费端代码(非 src/)禁止穿透 Block 边界访问内部
|
|
3
3
|
// 反例:querySelector('af-auth-form > div') / blockInstance.shadowRoot.querySelector(...)
|
|
4
4
|
// 正例:document.querySelector('af-auth-form').setAttribute('loading', 'true')
|
|
@@ -18,7 +18,8 @@ export default {
|
|
|
18
18
|
if (/src[\\/]|test[\\/]|scripts[\\/]/.test(filename)) return {};
|
|
19
19
|
|
|
20
20
|
// af-* 标签选择器(含子代/后代穿透):af-xxx > 或 af-xxx 空格 后跟其他选择器
|
|
21
|
-
|
|
21
|
+
// 标签名含连字符(af-auth-form),字符类须含 - 否则匹配不到组合器
|
|
22
|
+
const PENETRATE_RE = /af-[a-z][a-z0-9-]*(?:[>+~]|\s+[^,)\]]+)/;
|
|
22
23
|
|
|
23
24
|
return {
|
|
24
25
|
CallExpression(node) {
|
package/rules/wc-block-states.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3.5
|
|
1
|
+
// L3.5 af-mobile/wc-block-variant-enum(warn)
|
|
2
2
|
// 检测:Block 类的 variant 属性必须在 defineProp 时声明枚举值,或在 onAttributeChange 中校验
|
|
3
3
|
// 策略:检测 defineProp(proto, 'variant', { ... }) 调用,检查是否有 enum / values / 一行注释枚举
|
|
4
4
|
// 或:onAttributeChange 内有 variant 的 if/switch 校验
|
|
@@ -64,6 +64,15 @@ export default {
|
|
|
64
64
|
variantEnumDeclared = true;
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
|
+
// 赋值形式:AfFoo.prototype.onAttributeChange = function (name) { ... }
|
|
68
|
+
AssignmentExpression(node) {
|
|
69
|
+
const left = node.left;
|
|
70
|
+
if (left?.type !== 'MemberExpression' || left.property?.name !== 'onAttributeChange') return;
|
|
71
|
+
const bodyText = sourceCode.getText(node.right);
|
|
72
|
+
if (/variant/.test(bodyText) && /(if|switch)/.test(bodyText)) {
|
|
73
|
+
variantEnumDeclared = true;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
67
76
|
'Program:exit'() {
|
|
68
77
|
if (hasVariantProp && !variantEnumDeclared) {
|
|
69
78
|
context.report({ loc: { line: 1, column: 0 }, messageId: 'noEnum' });
|
package/rules/wc-cleanup.js
CHANGED
package/rules/wc-event-naming.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// L3-4
|
|
1
|
+
// L3-4 af-mobile/wc-event-naming(error,可自动修)
|
|
2
2
|
// 检测:emit('xxx') 调用名不匹配 /^af-[a-z0-9]+:[a-z]+$/
|
|
3
3
|
const EVENT_RE = /^af-[a-z0-9-]+:[a-z]+$/;
|
|
4
4
|
|
|
@@ -32,13 +32,18 @@ export default {
|
|
|
32
32
|
.toLowerCase();
|
|
33
33
|
// 如果不含冒号,尝试在组件名后插入冒号
|
|
34
34
|
if (!fixed.includes(':')) {
|
|
35
|
-
// 检测 af- 前缀
|
|
36
35
|
if (fixed.startsWith('af-')) {
|
|
37
|
-
//
|
|
38
|
-
|
|
36
|
+
// af-xxx-yyy → af-xxx:yyy(首个 - 起视为动作;动作段的 - 去除,保证 [a-z]+ 格式)
|
|
37
|
+
const rest = fixed.slice(3);
|
|
38
|
+
const dash = rest.indexOf('-');
|
|
39
|
+
if (dash > 0) {
|
|
40
|
+
fixed = `af-${rest.slice(0, dash)}:${rest.slice(dash + 1).replace(/-/g, '')}`;
|
|
41
|
+
} else {
|
|
42
|
+
fixed = 'af-component:' + rest;
|
|
43
|
+
}
|
|
39
44
|
} else {
|
|
40
45
|
// 无 af- 前缀,添加 af- 前缀 + 冒号
|
|
41
|
-
fixed = 'af-component:' + fixed;
|
|
46
|
+
fixed = 'af-component:' + fixed.replace(/-/g, '');
|
|
42
47
|
}
|
|
43
48
|
}
|
|
44
49
|
context.report({
|
package/rules/wc-part-naming.js
CHANGED
|
@@ -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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// af-mobile UI —— ESLint 规则共享工具
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { resolve, dirname } from 'node:path';
|
|
@@ -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
|
-
"
|
|
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,29 +23,33 @@
|
|
|
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",
|
|
31
|
+
"display",
|
|
27
32
|
"divider",
|
|
28
33
|
"empty",
|
|
34
|
+
"eyebrow",
|
|
29
35
|
"form-err",
|
|
30
36
|
"form-row",
|
|
31
37
|
"form-row-h",
|
|
32
38
|
"hero",
|
|
33
39
|
"input",
|
|
34
40
|
"input-bar",
|
|
41
|
+
"input-bar-fx",
|
|
35
42
|
"input-err",
|
|
36
43
|
"label",
|
|
37
44
|
"list",
|
|
38
45
|
"list-item",
|
|
39
|
-
"list-item-
|
|
46
|
+
"list-item-cp",
|
|
40
47
|
"meta",
|
|
41
48
|
"navbar",
|
|
42
49
|
"navbar-fixed",
|
|
43
50
|
"notice",
|
|
44
|
-
"notice-
|
|
45
|
-
"notice-
|
|
51
|
+
"notice-scr",
|
|
52
|
+
"notice-tx",
|
|
46
53
|
"page",
|
|
47
54
|
"page-col",
|
|
48
55
|
"price",
|
|
@@ -56,28 +63,32 @@
|
|
|
56
63
|
"radio-sm",
|
|
57
64
|
"rate",
|
|
58
65
|
"rate-lg",
|
|
59
|
-
"rate-
|
|
66
|
+
"rate-ro",
|
|
60
67
|
"rate-sm",
|
|
61
68
|
"rate-star",
|
|
69
|
+
"safe-bottom",
|
|
70
|
+
"safe-top",
|
|
71
|
+
"sb-clear",
|
|
72
|
+
"sb-icon",
|
|
73
|
+
"sb-wrap",
|
|
62
74
|
"scroll-y",
|
|
63
|
-
"search-bar-clear",
|
|
64
|
-
"search-bar-icon",
|
|
65
|
-
"search-bar-wrap",
|
|
66
75
|
"search-input",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
76
|
+
"section",
|
|
77
|
+
"section-tt",
|
|
78
|
+
"seg",
|
|
79
|
+
"seg-blk",
|
|
80
|
+
"seg-it",
|
|
70
81
|
"sheet",
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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",
|
|
81
92
|
"spinner",
|
|
82
93
|
"spinner-lg",
|
|
83
94
|
"spinner-sm",
|
|
@@ -90,44 +101,61 @@
|
|
|
90
101
|
"steps",
|
|
91
102
|
"subtitle",
|
|
92
103
|
"switch",
|
|
93
|
-
"switch-
|
|
104
|
+
"switch-ldg",
|
|
94
105
|
"switch-on",
|
|
95
106
|
"switch-sm",
|
|
96
|
-
"switch-
|
|
107
|
+
"switch-th",
|
|
97
108
|
"tab-item",
|
|
98
109
|
"tabbar",
|
|
99
110
|
"tabbar-fixed",
|
|
100
111
|
"tag",
|
|
101
112
|
"tag-danger",
|
|
113
|
+
"tag-lg",
|
|
114
|
+
"tag-md",
|
|
102
115
|
"tag-ok",
|
|
116
|
+
"tag-plain",
|
|
103
117
|
"tag-warn",
|
|
104
118
|
"textarea",
|
|
105
119
|
"thumb",
|
|
106
120
|
"title",
|
|
107
121
|
"toast",
|
|
108
|
-
"
|
|
109
|
-
"
|
|
122
|
+
"toast-error",
|
|
123
|
+
"toast-success",
|
|
124
|
+
"toast-warning",
|
|
125
|
+
"upload-gd",
|
|
126
|
+
"upload-tg"
|
|
110
127
|
],
|
|
111
128
|
"atomic": [
|
|
112
129
|
"aic",
|
|
113
130
|
"bg-brand",
|
|
131
|
+
"bg-card",
|
|
114
132
|
"bg-muted",
|
|
133
|
+
"ellipsis",
|
|
134
|
+
"ellipsis-2",
|
|
115
135
|
"f",
|
|
116
136
|
"fc",
|
|
137
|
+
"fi",
|
|
117
138
|
"flex-1",
|
|
118
139
|
"g-0",
|
|
119
140
|
"g-1",
|
|
120
141
|
"g-2",
|
|
121
142
|
"g-3",
|
|
122
143
|
"g-4",
|
|
144
|
+
"hl-b",
|
|
145
|
+
"hl-t",
|
|
123
146
|
"jcc",
|
|
124
147
|
"jce",
|
|
125
148
|
"jcsb",
|
|
149
|
+
"lh-normal",
|
|
150
|
+
"lh-tight",
|
|
151
|
+
"list",
|
|
126
152
|
"m-0",
|
|
127
153
|
"m-1",
|
|
128
154
|
"m-2",
|
|
129
155
|
"m-3",
|
|
130
156
|
"m-4",
|
|
157
|
+
"m-5",
|
|
158
|
+
"navbar",
|
|
131
159
|
"p-0",
|
|
132
160
|
"p-1",
|
|
133
161
|
"p-10",
|
|
@@ -136,6 +164,7 @@
|
|
|
136
164
|
"p-4",
|
|
137
165
|
"p-5",
|
|
138
166
|
"p-6",
|
|
167
|
+
"p-7",
|
|
139
168
|
"p-8",
|
|
140
169
|
"r-0",
|
|
141
170
|
"r-f",
|
|
@@ -145,13 +174,16 @@
|
|
|
145
174
|
"shadow-lg",
|
|
146
175
|
"shadow-md",
|
|
147
176
|
"shadow-sm",
|
|
177
|
+
"shrink-0",
|
|
148
178
|
"t-b",
|
|
149
179
|
"t-center",
|
|
180
|
+
"t-display",
|
|
150
181
|
"t-left",
|
|
151
182
|
"t-lg",
|
|
152
183
|
"t-m",
|
|
153
184
|
"t-md",
|
|
154
185
|
"t-right",
|
|
186
|
+
"t-semibold",
|
|
155
187
|
"t-sm",
|
|
156
188
|
"t-xl",
|
|
157
189
|
"t-xs",
|
|
@@ -174,6 +206,7 @@
|
|
|
174
206
|
"af-chart-line",
|
|
175
207
|
"af-chart-pie",
|
|
176
208
|
"af-chart-radar",
|
|
209
|
+
"af-chat",
|
|
177
210
|
"af-countdown",
|
|
178
211
|
"af-dialog",
|
|
179
212
|
"af-dropdown",
|
|
@@ -182,6 +215,8 @@
|
|
|
182
215
|
"af-list",
|
|
183
216
|
"af-navbar",
|
|
184
217
|
"af-notice-bar",
|
|
218
|
+
"af-number-keyboard",
|
|
219
|
+
"af-password-input",
|
|
185
220
|
"af-picker",
|
|
186
221
|
"af-progress",
|
|
187
222
|
"af-pull-refresh",
|
|
@@ -202,11 +237,22 @@
|
|
|
202
237
|
"--c-bg",
|
|
203
238
|
"--c-border",
|
|
204
239
|
"--c-brand",
|
|
240
|
+
"--c-brand-soft",
|
|
241
|
+
"--c-brand-strong",
|
|
205
242
|
"--c-card",
|
|
206
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",
|
|
207
252
|
"--c-muted",
|
|
208
253
|
"--c-muted-bg",
|
|
209
254
|
"--c-onbrand",
|
|
255
|
+
"--c-onwarn",
|
|
210
256
|
"--c-success",
|
|
211
257
|
"--c-text",
|
|
212
258
|
"--c-warn",
|
|
@@ -218,17 +264,32 @@
|
|
|
218
264
|
"--fw-bold",
|
|
219
265
|
"--fw-medium",
|
|
220
266
|
"--fw-normal",
|
|
267
|
+
"--fw-semibold",
|
|
268
|
+
"--lh-display",
|
|
221
269
|
"--lh-normal",
|
|
222
270
|
"--lh-tight",
|
|
271
|
+
"--ls-caption",
|
|
272
|
+
"--ls-display",
|
|
223
273
|
"--palette-bg",
|
|
224
274
|
"--palette-border",
|
|
225
275
|
"--palette-brand",
|
|
276
|
+
"--palette-brand-soft",
|
|
277
|
+
"--palette-brand-strong",
|
|
226
278
|
"--palette-card",
|
|
227
279
|
"--palette-color-scheme",
|
|
228
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",
|
|
229
289
|
"--palette-muted",
|
|
230
290
|
"--palette-muted-bg",
|
|
231
291
|
"--palette-onbrand",
|
|
292
|
+
"--palette-onwarn",
|
|
232
293
|
"--palette-shadow-lg",
|
|
233
294
|
"--palette-shadow-md",
|
|
234
295
|
"--palette-shadow-sm",
|
|
@@ -239,20 +300,28 @@
|
|
|
239
300
|
"--r-l",
|
|
240
301
|
"--r-m",
|
|
241
302
|
"--r-s",
|
|
303
|
+
"--r-xs",
|
|
304
|
+
"--ring-focus",
|
|
242
305
|
"--s-1",
|
|
243
306
|
"--s-2",
|
|
244
307
|
"--s-3",
|
|
245
308
|
"--s-4",
|
|
246
309
|
"--s-5",
|
|
247
310
|
"--s-6",
|
|
311
|
+
"--s-7",
|
|
312
|
+
"--s-8",
|
|
313
|
+
"--shadow-brand",
|
|
248
314
|
"--shadow-lg",
|
|
249
315
|
"--shadow-md",
|
|
250
316
|
"--shadow-sm",
|
|
317
|
+
"--t-display",
|
|
318
|
+
"--t-input",
|
|
251
319
|
"--t-lg",
|
|
252
320
|
"--t-md",
|
|
253
321
|
"--t-sm",
|
|
254
322
|
"--t-xl",
|
|
255
323
|
"--t-xs",
|
|
324
|
+
"--tabbar-h",
|
|
256
325
|
"--z-base",
|
|
257
326
|
"--z-dropdown",
|
|
258
327
|
"--z-modal",
|