@af-mobile/eslint-plugin 2.0.4 → 2.2.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/index.js +111 -95
- package/package.json +35 -31
- package/rules/atomic-duplicate.js +68 -60
- package/rules/k-for-require-key.js +33 -0
- package/rules/k-no-bare-and.js +31 -0
- package/rules/k-no-object-interpolation.js +31 -0
- package/rules/no-arbitrary-value.js +52 -52
- package/rules/no-emoji-icon.js +69 -0
- package/rules/no-inline-style.js +129 -127
- package/rules/no-recipe-break.js +59 -59
- package/rules/no-register-all.js +31 -31
- package/rules/no-tailwind-syntax.js +31 -31
- package/rules/no-token-modification.js +92 -92
- package/rules/no-variant-conflict.js +83 -72
- package/rules/prefer-component.js +58 -58
- package/rules/token-whitelist.js +84 -84
- package/rules/wc-aria-required.js +60 -60
- package/rules/wc-block-no-internal-ref.js +60 -60
- package/rules/wc-block-props-count.js +48 -48
- package/rules/wc-block-states.js +64 -64
- package/rules/wc-block-variant-enum.js +83 -83
- package/rules/wc-cleanup.js +75 -75
- package/rules/wc-event-naming.js +60 -60
- package/rules/wc-light-no-style.js +87 -87
- package/rules/wc-part-naming.js +33 -33
- package/rules/wc-shadow-use-token.js +72 -72
- package/utils/aria-requirements.json +101 -101
- package/utils/helpers.js +106 -84
- package/utils/whitelist-v1.json +41 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// L2-8 af-mobile/no-emoji-icon(warn)
|
|
2
|
+
// 检测 emoji 当图标——"廉价感"第一视觉来源(v1.6.1 规则化,配套 prompt 图标规范)
|
|
3
|
+
// 检测点(低误报,只查图标语境):
|
|
4
|
+
// 1. icon 属性值:tabs/tabbar 配置对象 { label: 'x', icon: '📋' }(旧 few-shot 示例的反模式)
|
|
5
|
+
// 2. HTML 字符串中 tab-item 元素 / data-role="icon" 元素的文本内容含 emoji
|
|
6
|
+
// 修正:用 24px stroke SVG(prompt 图标 path 库);af-tabbar 的 icon 仅支持文本字符,需要图标时省略 icon 字段
|
|
7
|
+
const EMOJI_RE = /\p{Extended_Pictographic}/u;
|
|
8
|
+
|
|
9
|
+
function firstEmoji(str) {
|
|
10
|
+
const m = str.match(EMOJI_RE);
|
|
11
|
+
return m ? m[0] : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// HTML 字符串中的图标语境:tab-item 元素 / data-role="icon" 元素(取开闭标签之间的纯文本段)
|
|
15
|
+
const ICON_CONTEXT_RES = [
|
|
16
|
+
/<[\w-]+[^>]*\bclass="[^"]*\btab-item\b[^"]*"[^>]*>([^<]*)</g,
|
|
17
|
+
/<[\w-]+[^>]*\bdata-role="icon"[^>]*>([^<]*)</g,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function checkHtmlString(str, node, report) {
|
|
21
|
+
for (const re of ICON_CONTEXT_RES) {
|
|
22
|
+
re.lastIndex = 0;
|
|
23
|
+
let m;
|
|
24
|
+
while ((m = re.exec(str))) {
|
|
25
|
+
const emoji = firstEmoji(m[1]);
|
|
26
|
+
if (emoji) report(node, emoji);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: { description: '禁止 emoji 当图标(用 24px stroke SVG)' },
|
|
35
|
+
schema: [],
|
|
36
|
+
messages: {
|
|
37
|
+
emojiIcon: "Emoji {{emoji}} used as icon — use a 24px stroke SVG instead; af-tabbar's icon prop only supports text characters",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const report = (node, emoji) => context.report({ node, messageId: 'emojiIcon', data: { emoji } });
|
|
42
|
+
return {
|
|
43
|
+
// { icon: '📋' } 配置对象(af-tabbar / af-tabs 的 tabs 数组)
|
|
44
|
+
Property(node) {
|
|
45
|
+
const keyName = node.key.type === 'Identifier'
|
|
46
|
+
? node.key.name
|
|
47
|
+
: (node.key.type === 'Literal' ? String(node.key.value) : null);
|
|
48
|
+
if (keyName !== 'icon') return;
|
|
49
|
+
const v = node.value;
|
|
50
|
+
if (v.type === 'Literal' && typeof v.value === 'string') {
|
|
51
|
+
const emoji = firstEmoji(v.value);
|
|
52
|
+
if (emoji) report(v, emoji);
|
|
53
|
+
} else if (v.type === 'TemplateLiteral' && v.expressions.length === 0) {
|
|
54
|
+
const emoji = firstEmoji(v.quasis[0].value.raw);
|
|
55
|
+
if (emoji) report(v, emoji);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// HTML 字符串(含 template literal)中的图标语境元素
|
|
59
|
+
Literal(node) {
|
|
60
|
+
if (typeof node.value !== 'string') return;
|
|
61
|
+
checkHtmlString(node.value, node, report);
|
|
62
|
+
},
|
|
63
|
+
TemplateElement(node) {
|
|
64
|
+
if (!node.value || !node.value.raw) return;
|
|
65
|
+
checkHtmlString(node.value.raw, node, report);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
package/rules/no-inline-style.js
CHANGED
|
@@ -1,127 +1,129 @@
|
|
|
1
|
-
// L1-2 af-mobile/no-inline-style(error,部分可自动修)
|
|
2
|
-
// 检测:style="" 中设置 forbiddenInlineStyle 列表内的属性
|
|
3
|
-
// 例外:display/transform/z-index/width/height 等布局属性放行;skeleton class 元素内 width/height 放行
|
|
4
|
-
// 适用:HTML/JS 文件中所有字符串字面量(含 template literal),覆盖组件 innerHTML 赋值场景
|
|
5
|
-
import { readFileSync } from 'node:fs';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { resolve, dirname } from 'node:path';
|
|
8
|
-
|
|
9
|
-
// 加载 whitelist(与规则文件同级目录 ../utils/whitelist-v1.json)
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
-
const whitelistPath = resolve(__dirname, '../utils/whitelist-v1.json');
|
|
12
|
-
const whitelist = JSON.parse(readFileSync(whitelistPath, 'utf8'));
|
|
13
|
-
const FORBIDDEN = new Set(whitelist.forbiddenInlineStyle);
|
|
14
|
-
|
|
15
|
-
// 布局类属性放行(设计文档 L1-2 例外)
|
|
16
|
-
const ALLOWED_LAYOUT = new Set([
|
|
17
|
-
'display', 'transform', 'z-index', 'width', 'height',
|
|
18
|
-
'position', 'top', 'right', 'bottom', 'left',
|
|
19
|
-
'flex', 'flex-direction', 'flex-wrap', 'justify-content', 'align-items',
|
|
20
|
-
'grid-template-columns', 'grid-template-rows', 'gap',
|
|
21
|
-
'overflow', 'overflow-x', 'overflow-y',
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
// 提取 style="..." 内容中的属性名
|
|
25
|
-
function extractStyleProps(styleContent) {
|
|
26
|
-
const props = [];
|
|
27
|
-
// 匹配 prop-name: value; 形式
|
|
28
|
-
const re = /([a-z-]+)\s*:/g;
|
|
29
|
-
let m;
|
|
30
|
-
while ((m = re.exec(styleContent))) props.push(m[1].toLowerCase());
|
|
31
|
-
return props;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// 检测 style="" 字符串中的 forbidden 属性
|
|
35
|
-
function checkStyleString(str) {
|
|
36
|
-
const violations = [];
|
|
37
|
-
const styleRe = /style\s*=\s*"([^"]*)"/g;
|
|
38
|
-
let m;
|
|
39
|
-
while ((m = styleRe.exec(str))) {
|
|
40
|
-
const styleContent = m[1];
|
|
41
|
-
const props = extractStyleProps(styleContent);
|
|
42
|
-
for (const prop of props) {
|
|
43
|
-
if (FORBIDDEN.has(prop)) {
|
|
44
|
-
violations.push({ prop, value: styleContent });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return violations;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default {
|
|
52
|
-
meta: {
|
|
53
|
-
type: 'problem',
|
|
54
|
-
docs: {
|
|
55
|
-
description: '禁止 inline style 中使用 forbidden 属性(应使用 L2 class)',
|
|
56
|
-
},
|
|
57
|
-
schema: [{
|
|
58
|
-
type: 'object',
|
|
59
|
-
properties: {
|
|
60
|
-
allowProperties: {
|
|
61
|
-
type: 'array',
|
|
62
|
-
items: { type: 'string' },
|
|
63
|
-
description: '项目级额外放行的属性名',
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
}],
|
|
68
|
-
messages: {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (callee
|
|
102
|
-
if (callee.
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
106
|
-
if (propArg.value
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (node.left
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
};
|
|
1
|
+
// L1-2 af-mobile/no-inline-style(error,部分可自动修)
|
|
2
|
+
// 检测:style="" 中设置 forbiddenInlineStyle 列表内的属性
|
|
3
|
+
// 例外:display/transform/z-index/width/height 等布局属性放行;skeleton class 元素内 width/height 放行
|
|
4
|
+
// 适用:HTML/JS 文件中所有字符串字面量(含 template literal),覆盖组件 innerHTML 赋值场景
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
|
|
9
|
+
// 加载 whitelist(与规则文件同级目录 ../utils/whitelist-v1.json)
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const whitelistPath = resolve(__dirname, '../utils/whitelist-v1.json');
|
|
12
|
+
const whitelist = JSON.parse(readFileSync(whitelistPath, 'utf8'));
|
|
13
|
+
const FORBIDDEN = new Set(whitelist.forbiddenInlineStyle);
|
|
14
|
+
|
|
15
|
+
// 布局类属性放行(设计文档 L1-2 例外)
|
|
16
|
+
const ALLOWED_LAYOUT = new Set([
|
|
17
|
+
'display', 'transform', 'z-index', 'width', 'height',
|
|
18
|
+
'position', 'top', 'right', 'bottom', 'left',
|
|
19
|
+
'flex', 'flex-direction', 'flex-wrap', 'justify-content', 'align-items',
|
|
20
|
+
'grid-template-columns', 'grid-template-rows', 'gap',
|
|
21
|
+
'overflow', 'overflow-x', 'overflow-y',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
// 提取 style="..." 内容中的属性名
|
|
25
|
+
function extractStyleProps(styleContent) {
|
|
26
|
+
const props = [];
|
|
27
|
+
// 匹配 prop-name: value; 形式
|
|
28
|
+
const re = /([a-z-]+)\s*:/g;
|
|
29
|
+
let m;
|
|
30
|
+
while ((m = re.exec(styleContent))) props.push(m[1].toLowerCase());
|
|
31
|
+
return props;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 检测 style="" 字符串中的 forbidden 属性
|
|
35
|
+
function checkStyleString(str) {
|
|
36
|
+
const violations = [];
|
|
37
|
+
const styleRe = /style\s*=\s*"([^"]*)"/g;
|
|
38
|
+
let m;
|
|
39
|
+
while ((m = styleRe.exec(str))) {
|
|
40
|
+
const styleContent = m[1];
|
|
41
|
+
const props = extractStyleProps(styleContent);
|
|
42
|
+
for (const prop of props) {
|
|
43
|
+
if (FORBIDDEN.has(prop)) {
|
|
44
|
+
violations.push({ prop, value: styleContent });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return violations;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description: '禁止 inline style 中使用 forbidden 属性(应使用 L2 class)',
|
|
56
|
+
},
|
|
57
|
+
schema: [{
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
allowProperties: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
items: { type: 'string' },
|
|
63
|
+
description: '项目级额外放行的属性名',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
}],
|
|
68
|
+
messages: {
|
|
69
|
+
// v1.6.1:报错带具体违规属性名(旧报错只给整段 style,AI 修正时只能整段删掉重猜,加剧样式削平)
|
|
70
|
+
forbidden: "Inline style property '{{prop}}' is forbidden in '{{value}}'. Use class instead",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
create(context) {
|
|
75
|
+
const options = context.options[0] || {};
|
|
76
|
+
const extraAllow = new Set(options.allowProperties || []);
|
|
77
|
+
const nodeText = (node) => (context.sourceCode || context.getSourceCode()).getText(node);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
// 检测字符串字面量(含 template literal)
|
|
81
|
+
Literal(node) {
|
|
82
|
+
if (typeof node.value !== 'string') return;
|
|
83
|
+
const violations = checkStyleString(node.value);
|
|
84
|
+
for (const v of violations) {
|
|
85
|
+
if (extraAllow.has(v.prop)) continue;
|
|
86
|
+
context.report({ node, messageId: 'forbidden', data: { prop: v.prop, value: v.value } });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
TemplateElement(node) {
|
|
90
|
+
if (!node.value || !node.value.raw) return;
|
|
91
|
+
const violations = checkStyleString(node.value.raw);
|
|
92
|
+
for (const v of violations) {
|
|
93
|
+
if (extraAllow.has(v.prop)) continue;
|
|
94
|
+
context.report({ node, messageId: 'forbidden', data: { prop: v.prop, value: v.value } });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
// 检测 el.style.setProperty('color', ...) / el.style.xxx = '...' 绕过
|
|
98
|
+
// 仅当属性名为 forbidden 视觉属性且非常量 --* CSS 变量时报告
|
|
99
|
+
CallExpression(node) {
|
|
100
|
+
const callee = node.callee;
|
|
101
|
+
if (callee?.type !== 'MemberExpression') return;
|
|
102
|
+
if (callee.object?.type !== 'MemberExpression') return;
|
|
103
|
+
if (callee.object.property?.name !== 'style') return;
|
|
104
|
+
if (callee.property?.name !== 'setProperty') return;
|
|
105
|
+
const propArg = node.arguments?.[0];
|
|
106
|
+
if (!propArg || propArg.type !== 'Literal' || typeof propArg.value !== 'string') return;
|
|
107
|
+
// CSS 自定义属性(--*)豁免:用于主题变量传递,由 no-token-modification 规则单独管
|
|
108
|
+
if (propArg.value.startsWith('--')) return;
|
|
109
|
+
const prop = propArg.value.toLowerCase();
|
|
110
|
+
if (FORBIDDEN.has(prop) && !ALLOWED_LAYOUT.has(prop) && !extraAllow.has(prop)) {
|
|
111
|
+
context.report({ node, messageId: 'forbidden', data: { prop, value: nodeText(node) } });
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
AssignmentExpression(node) {
|
|
115
|
+
// el.style.color = 'red' 形式
|
|
116
|
+
if (node.left?.type !== 'MemberExpression') return;
|
|
117
|
+
if (node.left.object?.type !== 'MemberExpression') return;
|
|
118
|
+
if (node.left.object.property?.name !== 'style') return;
|
|
119
|
+
const propName = node.left.property?.name;
|
|
120
|
+
if (!propName) return;
|
|
121
|
+
// kebab-case 转换:backgroundColor → background-color
|
|
122
|
+
const kebab = propName.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
123
|
+
if (FORBIDDEN.has(kebab) && !ALLOWED_LAYOUT.has(kebab) && !extraAllow.has(kebab)) {
|
|
124
|
+
context.report({ node, messageId: 'forbidden', data: { prop: kebab, value: nodeText(node) } });
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|
package/rules/no-recipe-break.js
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
-
// L2-2 af-mobile/no-recipe-break(error,4 子规则)
|
|
2
|
-
// a: .cell + f/fi/fc 原子 → 布局冲突
|
|
3
|
-
// b: .btn(非 .btn-ghost)+ text-brand/text-danger/text-success → 对比度问题
|
|
4
|
-
// c: .input + t-sm/t-xs → iOS 焦点缩放
|
|
5
|
-
// d: .btn(非 .btn-ghost)+ bg-card/bg-muted → 背景覆盖破坏 onbrand 对比度
|
|
6
|
-
import { extractAllClassLists } from '../utils/helpers.js';
|
|
7
|
-
|
|
8
|
-
const FLEX_ATOMS = new Set(['f', 'fi', 'fc']);
|
|
9
|
-
const COLORED_TEXT = new Set(['text-brand', 'text-danger', 'text-success']);
|
|
10
|
-
const SMALL_FONT = new Set(['t-sm', 't-xs']);
|
|
11
|
-
const BROKEN_BG = new Set(['bg-card', 'bg-muted']);
|
|
12
|
-
|
|
13
|
-
export default {
|
|
14
|
-
meta: {
|
|
15
|
-
type: 'problem',
|
|
16
|
-
docs: { description: '禁止原子类破坏 recipe 内置样式' },
|
|
17
|
-
schema: [],
|
|
18
|
-
messages: {
|
|
19
|
-
cellFlex: ".cell has built-in display:flex, adding 'f'/'fc' may break layout",
|
|
20
|
-
btnColor: ".btn background uses --c-brand, text color must keep --c-onbrand contrast. Use .btn-ghost instead for colored text",
|
|
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",
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
create(context) {
|
|
26
|
-
function checkClasses(classes, node) {
|
|
27
|
-
const set = new Set(classes);
|
|
28
|
-
// a: .cell + f/fi/fc
|
|
29
|
-
if (set.has('cell')) {
|
|
30
|
-
for (const a of FLEX_ATOMS) {
|
|
31
|
-
if (set.has(a)) { context.report({ node, messageId: 'cellFlex' }); break; }
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
// b: .btn(非 .btn-ghost)+ colored text
|
|
35
|
-
if (set.has('btn') && !set.has('btn-ghost')) {
|
|
36
|
-
for (const a of COLORED_TEXT) {
|
|
37
|
-
if (set.has(a)) { context.report({ node, messageId: 'btnColor' }); break; }
|
|
38
|
-
}
|
|
39
|
-
// d: 背景覆盖同样破坏 onbrand 对比度
|
|
40
|
-
for (const a of BROKEN_BG) {
|
|
41
|
-
if (set.has(a)) { context.report({ node, messageId: 'btnBg' }); break; }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
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
|
-
for (const a of SMALL_FONT) {
|
|
47
|
-
if (set.has(a)) { context.report({ node, messageId: 'inputFont' }); break; }
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function checkString(str, node) {
|
|
52
|
-
for (const { classes } of extractAllClassLists(str)) checkClasses(classes, node);
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
56
|
-
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
57
|
-
};
|
|
58
|
-
},
|
|
59
|
-
};
|
|
1
|
+
// L2-2 af-mobile/no-recipe-break(error,4 子规则)
|
|
2
|
+
// a: .cell + f/fi/fc 原子 → 布局冲突
|
|
3
|
+
// b: .btn(非 .btn-ghost)+ text-brand/text-danger/text-success → 对比度问题
|
|
4
|
+
// c: .input + t-sm/t-xs → iOS 焦点缩放
|
|
5
|
+
// d: .btn(非 .btn-ghost)+ bg-card/bg-muted → 背景覆盖破坏 onbrand 对比度
|
|
6
|
+
import { extractAllClassLists } from '../utils/helpers.js';
|
|
7
|
+
|
|
8
|
+
const FLEX_ATOMS = new Set(['f', 'fi', 'fc']);
|
|
9
|
+
const COLORED_TEXT = new Set(['text-brand', 'text-danger', 'text-success']);
|
|
10
|
+
const SMALL_FONT = new Set(['t-sm', 't-xs']);
|
|
11
|
+
const BROKEN_BG = new Set(['bg-card', 'bg-muted']);
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: { description: '禁止原子类破坏 recipe 内置样式' },
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
cellFlex: ".cell has built-in display:flex, adding 'f'/'fc' may break layout",
|
|
20
|
+
btnColor: ".btn background uses --c-brand, text color must keep --c-onbrand contrast. Use .btn-ghost instead for colored text",
|
|
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",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
function checkClasses(classes, node) {
|
|
27
|
+
const set = new Set(classes);
|
|
28
|
+
// a: .cell + f/fi/fc
|
|
29
|
+
if (set.has('cell')) {
|
|
30
|
+
for (const a of FLEX_ATOMS) {
|
|
31
|
+
if (set.has(a)) { context.report({ node, messageId: 'cellFlex' }); break; }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// b: .btn(非 .btn-ghost)+ colored text
|
|
35
|
+
if (set.has('btn') && !set.has('btn-ghost')) {
|
|
36
|
+
for (const a of COLORED_TEXT) {
|
|
37
|
+
if (set.has(a)) { context.report({ node, messageId: 'btnColor' }); break; }
|
|
38
|
+
}
|
|
39
|
+
// d: 背景覆盖同样破坏 onbrand 对比度
|
|
40
|
+
for (const a of BROKEN_BG) {
|
|
41
|
+
if (set.has(a)) { context.report({ node, messageId: 'btnBg' }); break; }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
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
|
+
for (const a of SMALL_FONT) {
|
|
47
|
+
if (set.has(a)) { context.report({ node, messageId: 'inputFont' }); break; }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function checkString(str, node) {
|
|
52
|
+
for (const { classes } of extractAllClassLists(str)) checkClasses(classes, node);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
56
|
+
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
package/rules/no-register-all.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
// L3 af-mobile/no-register-all(error)
|
|
2
|
-
// 检测:registerAll() 调用(已废弃/已移除,诱导全量加载,失去 Tree Shaking)
|
|
3
|
-
// 正确:register('af-list', 'af-dialog') 显式列名,配合 Tree Shaking 按需打包
|
|
4
|
-
export default {
|
|
5
|
-
meta: {
|
|
6
|
-
type: 'problem',
|
|
7
|
-
docs: { description: '禁止 registerAll(),改用 register(...names) 显式列名', fixable: 'ai-rewrite' },
|
|
8
|
-
schema: [],
|
|
9
|
-
messages: {
|
|
10
|
-
registerAll: "registerAll() 已废弃/已移除(全量注册 = 全局引入),改用 register(...names) 显式列名",
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
create(context) {
|
|
14
|
-
const filename = context.filename || context.getFilename();
|
|
15
|
-
// 非消费端放行:单元测试夹具 / 构建脚本
|
|
16
|
-
// 注意:原正则含 `src[\\/]` 会误伤消费端 src/main.js——消费端恰恰是要约束的对象。
|
|
17
|
-
// 库源码 src/ 不在此 skip 也安全:库源码已移除 registerAll(按需引入铁律)。
|
|
18
|
-
if (/test[\\/]|scripts[\\/]/.test(filename)) return {};
|
|
19
|
-
return {
|
|
20
|
-
CallExpression(node) {
|
|
21
|
-
const callee = node.callee;
|
|
22
|
-
const name = callee.type === 'Identifier' ? callee.name
|
|
23
|
-
: callee.type === 'MemberExpression' && callee.property.type === 'Identifier' ? callee.property.name
|
|
24
|
-
: null;
|
|
25
|
-
if (name === 'registerAll') {
|
|
26
|
-
context.report({ node, messageId: 'registerAll' });
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
};
|
|
1
|
+
// L3 af-mobile/no-register-all(error)
|
|
2
|
+
// 检测:registerAll() 调用(已废弃/已移除,诱导全量加载,失去 Tree Shaking)
|
|
3
|
+
// 正确:register('af-list', 'af-dialog') 显式列名,配合 Tree Shaking 按需打包
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'problem',
|
|
7
|
+
docs: { description: '禁止 registerAll(),改用 register(...names) 显式列名', fixable: 'ai-rewrite' },
|
|
8
|
+
schema: [],
|
|
9
|
+
messages: {
|
|
10
|
+
registerAll: "registerAll() 已废弃/已移除(全量注册 = 全局引入),改用 register(...names) 显式列名",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
const filename = context.filename || context.getFilename();
|
|
15
|
+
// 非消费端放行:单元测试夹具 / 构建脚本
|
|
16
|
+
// 注意:原正则含 `src[\\/]` 会误伤消费端 src/main.js——消费端恰恰是要约束的对象。
|
|
17
|
+
// 库源码 src/ 不在此 skip 也安全:库源码已移除 registerAll(按需引入铁律)。
|
|
18
|
+
if (/test[\\/]|scripts[\\/]/.test(filename)) return {};
|
|
19
|
+
return {
|
|
20
|
+
CallExpression(node) {
|
|
21
|
+
const callee = node.callee;
|
|
22
|
+
const name = callee.type === 'Identifier' ? callee.name
|
|
23
|
+
: callee.type === 'MemberExpression' && callee.property.type === 'Identifier' ? callee.property.name
|
|
24
|
+
: null;
|
|
25
|
+
if (name === 'registerAll') {
|
|
26
|
+
context.report({ node, messageId: 'registerAll' });
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
// L2-5 af-mobile/no-tailwind-syntax(error)
|
|
2
|
-
// 检测:class 含 sm:/md:/lg:/xl:/hover:/focus:/active:/dark: 前缀
|
|
3
|
-
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
|
-
|
|
5
|
-
const TAILWIND_PREFIX = /^(sm|md|lg|xl|hover|focus|active|dark):/;
|
|
6
|
-
|
|
7
|
-
export default {
|
|
8
|
-
meta: {
|
|
9
|
-
type: 'problem',
|
|
10
|
-
docs: { description: '禁止 Tailwind 响应式/状态前缀语法' },
|
|
11
|
-
schema: [],
|
|
12
|
-
messages: {
|
|
13
|
-
tailwind: "'{{name}}' responsive/state prefix syntax forbidden. Use @container in recipes.project.css for container-queries responsive behavior",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
create(context) {
|
|
17
|
-
function checkString(str, node) {
|
|
18
|
-
for (const { classes } of extractAllClassLists(str)) {
|
|
19
|
-
for (const cls of classes) {
|
|
20
|
-
if (TAILWIND_PREFIX.test(cls)) {
|
|
21
|
-
context.report({ node, messageId: 'tailwind', data: { name: cls } });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
28
|
-
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
29
|
-
};
|
|
30
|
-
},
|
|
31
|
-
};
|
|
1
|
+
// L2-5 af-mobile/no-tailwind-syntax(error)
|
|
2
|
+
// 检测:class 含 sm:/md:/lg:/xl:/hover:/focus:/active:/dark: 前缀
|
|
3
|
+
import { extractAllClassLists } from '../utils/helpers.js';
|
|
4
|
+
|
|
5
|
+
const TAILWIND_PREFIX = /^(sm|md|lg|xl|hover|focus|active|dark):/;
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
meta: {
|
|
9
|
+
type: 'problem',
|
|
10
|
+
docs: { description: '禁止 Tailwind 响应式/状态前缀语法' },
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
tailwind: "'{{name}}' responsive/state prefix syntax forbidden. Use @container in recipes.project.css for container-queries responsive behavior",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
function checkString(str, node) {
|
|
18
|
+
for (const { classes } of extractAllClassLists(str)) {
|
|
19
|
+
for (const cls of classes) {
|
|
20
|
+
if (TAILWIND_PREFIX.test(cls)) {
|
|
21
|
+
context.report({ node, messageId: 'tailwind', data: { name: cls } });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
Literal(node) { if (typeof node.value === 'string') checkString(node.value, node); },
|
|
28
|
+
TemplateElement(node) { if (node.value?.raw) checkString(node.value.raw, node); },
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
};
|