@lark-apaas/fullstack-presets 1.1.5-alpha.2 → 1.1.5-alpha.21
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/lib/custom-eslint-rules/index.d.ts +2 -0
- package/lib/custom-eslint-rules/index.js +4 -0
- package/lib/custom-eslint-rules/no-direct-capability-api.d.ts +16 -0
- package/lib/custom-eslint-rules/no-direct-capability-api.js +119 -0
- package/lib/custom-eslint-rules/require-app-container.d.ts +7 -1
- package/lib/custom-eslint-rules/require-app-container.js +48 -13
- package/lib/custom-eslint-rules/require-scroll-reveal-hook.d.ts +17 -0
- package/lib/custom-eslint-rules/require-scroll-reveal-hook.js +135 -0
- package/lib/custom-stylelint-rules/hsl-variable.d.ts +41 -0
- package/lib/custom-stylelint-rules/hsl-variable.js +205 -0
- package/lib/recommend/eslint/eslint-client.d.ts +2 -0
- package/lib/recommend/eslint/eslint-client.js +10 -24
- package/lib/recommend/eslint/index.d.ts +2 -0
- package/lib/recommend/stylelint/index.d.ts +2 -0
- package/lib/recommend/stylelint/index.js +37 -0
- package/lib/simple/recommend/eslint/eslint-client.d.ts +5 -3
- package/lib/simple/recommend/eslint/eslint-client.js +64 -31
- package/lib/simple/recommend/eslint/eslint-server.d.ts +3 -0
- package/lib/simple/recommend/eslint/eslint-server.js +3 -0
- package/lib/simple/recommend/eslint/index.d.ts +5 -3
- package/lib/simple/recommend/stylelint/index.d.ts +2 -0
- package/lib/simple/recommend/stylelint/index.js +37 -0
- package/package.json +6 -2
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export declare const customRules: {
|
|
2
2
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
3
3
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
4
|
+
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
5
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
4
6
|
};
|
|
@@ -6,7 +6,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.customRules = void 0;
|
|
7
7
|
const no_nested_styled_jsx_1 = __importDefault(require("./no-nested-styled-jsx"));
|
|
8
8
|
const require_app_container_1 = __importDefault(require("./require-app-container"));
|
|
9
|
+
const no_direct_capability_api_1 = __importDefault(require("./no-direct-capability-api"));
|
|
10
|
+
const require_scroll_reveal_hook_1 = __importDefault(require("./require-scroll-reveal-hook"));
|
|
9
11
|
exports.customRules = {
|
|
10
12
|
'no-nested-styled-jsx': no_nested_styled_jsx_1.default,
|
|
11
13
|
'require-app-container': require_app_container_1.default,
|
|
14
|
+
'no-direct-capability-api': no_direct_capability_api_1.default,
|
|
15
|
+
'require-scroll-reveal-hook': require_scroll_reveal_hook_1.default,
|
|
12
16
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:禁止直接使用 axios 调用 /__innerapi__/capability/ 路径
|
|
3
|
+
*
|
|
4
|
+
* 此规则确保开发者使用 capabilityClient.load().call() 方式调用能力,
|
|
5
|
+
* 而不是直接通过 axios 调用内部 API 路径。
|
|
6
|
+
*
|
|
7
|
+
* 错误示例:
|
|
8
|
+
* axios.post('/__innerapi__/capability/xxx')
|
|
9
|
+
* axiosForBackend.get('/__innerapi__/capability/list')
|
|
10
|
+
*
|
|
11
|
+
* 正确示例:
|
|
12
|
+
* capabilityClient.load('capabilityId').call('action', params)
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from 'eslint';
|
|
15
|
+
declare const rule: Rule.RuleModule;
|
|
16
|
+
export default rule;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const DEFAULT_MESSAGE = "Direct API calls to /__innerapi__/capability/ are not allowed. Use capabilityClient.load('capabilityId').call('action', params) instead.";
|
|
4
|
+
// 匹配 capability 内部 API 路径的正则表达式
|
|
5
|
+
const CAPABILITY_PATH_PATTERN = /__innerapi__\/capability\//;
|
|
6
|
+
// HTTP 方法列表
|
|
7
|
+
const HTTP_METHODS = [
|
|
8
|
+
'get',
|
|
9
|
+
'post',
|
|
10
|
+
'put',
|
|
11
|
+
'delete',
|
|
12
|
+
'patch',
|
|
13
|
+
'request',
|
|
14
|
+
'head',
|
|
15
|
+
'options',
|
|
16
|
+
];
|
|
17
|
+
// 需要检测的 axios 标识符
|
|
18
|
+
const AXIOS_IDENTIFIERS = ['axios', 'axiosForBackend'];
|
|
19
|
+
/**
|
|
20
|
+
* 从 AST 节点中提取 URL 字符串值
|
|
21
|
+
* 支持:字符串字面量、模板字符串、字符串拼接
|
|
22
|
+
*/
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
24
|
+
function extractUrlValue(node) {
|
|
25
|
+
if (!node)
|
|
26
|
+
return null;
|
|
27
|
+
switch (node.type) {
|
|
28
|
+
case 'Literal':
|
|
29
|
+
return typeof node.value === 'string' ? node.value : null;
|
|
30
|
+
case 'TemplateLiteral':
|
|
31
|
+
// 拼接所有静态部分(quasis)
|
|
32
|
+
return node.quasis.map((q) => q.value.raw).join('');
|
|
33
|
+
case 'BinaryExpression':
|
|
34
|
+
// 处理字符串拼接 'a' + 'b'
|
|
35
|
+
if (node.operator === '+') {
|
|
36
|
+
const left = extractUrlValue(node.left);
|
|
37
|
+
const right = extractUrlValue(node.right);
|
|
38
|
+
if (left !== null || right !== null) {
|
|
39
|
+
return (left || '') + (right || '');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
default:
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const rule = {
|
|
48
|
+
meta: {
|
|
49
|
+
type: 'problem',
|
|
50
|
+
docs: {
|
|
51
|
+
description: '禁止直接使用 axios 调用 /__innerapi__/capability/ 路径',
|
|
52
|
+
category: 'Best Practices',
|
|
53
|
+
recommended: true,
|
|
54
|
+
},
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
message: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
description: '自定义错误信息',
|
|
62
|
+
},
|
|
63
|
+
axiosIdentifiers: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
items: { type: 'string' },
|
|
66
|
+
description: '额外需要检测的 axios 实例标识符',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
messages: {
|
|
73
|
+
noDirectCapabilityApi: DEFAULT_MESSAGE,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
create(context) {
|
|
77
|
+
const options = context.options[0];
|
|
78
|
+
const customMessage = options?.message;
|
|
79
|
+
const additionalIdentifiers = options?.axiosIdentifiers || [];
|
|
80
|
+
// 合并默认和自定义的 axios 标识符
|
|
81
|
+
const allAxiosIdentifiers = [
|
|
82
|
+
...AXIOS_IDENTIFIERS,
|
|
83
|
+
...additionalIdentifiers,
|
|
84
|
+
];
|
|
85
|
+
return {
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
87
|
+
CallExpression(node) {
|
|
88
|
+
// 1. 检查是否为 MemberExpression 调用 (xxx.method())
|
|
89
|
+
if (node.callee.type !== 'MemberExpression')
|
|
90
|
+
return;
|
|
91
|
+
// 2. 检查方法名是否为 HTTP 方法
|
|
92
|
+
const methodName = node.callee.property?.name;
|
|
93
|
+
if (!methodName || !HTTP_METHODS.includes(methodName.toLowerCase())) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// 3. 检查调用者是否为 axios 相关标识符
|
|
97
|
+
const objectName = node.callee.object?.name;
|
|
98
|
+
if (!objectName || !allAxiosIdentifiers.includes(objectName)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// 4. 获取第一个参数(URL)
|
|
102
|
+
const urlArg = node.arguments[0];
|
|
103
|
+
if (!urlArg)
|
|
104
|
+
return;
|
|
105
|
+
// 5. 提取 URL 并检查是否包含 capability 路径
|
|
106
|
+
const urlValue = extractUrlValue(urlArg);
|
|
107
|
+
if (urlValue && CAPABILITY_PATH_PATTERN.test(urlValue)) {
|
|
108
|
+
context.report({
|
|
109
|
+
node,
|
|
110
|
+
...(customMessage
|
|
111
|
+
? { message: customMessage }
|
|
112
|
+
: { messageId: 'noDirectCapabilityApi' }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
exports.default = rule;
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ESLint 规则:要求入口文件中必须包含 AppContainer 组件
|
|
3
3
|
*
|
|
4
|
-
* 此规则确保 client/src/
|
|
4
|
+
* 此规则确保 client/src/app.tsx 或 client/src/index.tsx 中
|
|
5
5
|
* 包含 AppContainer 组件,以保证平台功能正常运行。
|
|
6
|
+
*
|
|
7
|
+
* 跨文件检查逻辑:
|
|
8
|
+
* - 只在 app.tsx 上执行检查和报错
|
|
9
|
+
* - 如果 app.tsx 没有 AppContainer,会检查 index.tsx
|
|
10
|
+
* - 只要任一文件使用了 AppContainer 就算通过
|
|
11
|
+
* - 只有两个文件都没有时才在 app.tsx 上报错
|
|
6
12
|
*/
|
|
7
13
|
import type { Rule } from 'eslint';
|
|
8
14
|
declare const rule: Rule.RuleModule;
|
|
@@ -34,11 +34,33 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const path = __importStar(require("path"));
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const DEFAULT_MESSAGE = 'Missing platform component AppContainer. Please ensure that App is wrapped with AppContainer (in app.tsx or index.tsx) to enable platform features.';
|
|
39
|
+
// 主检查文件(报错在此文件上)
|
|
40
|
+
const PRIMARY_FILE = 'app.tsx';
|
|
41
|
+
// 备选检查文件
|
|
42
|
+
const SECONDARY_FILE = 'index.tsx';
|
|
40
43
|
// 匹配 client/src/ 目录的正则表达式
|
|
41
44
|
const CLIENT_SRC_PATTERN = /[/\\]client[/\\]src[/\\]$/;
|
|
45
|
+
// 检测文件内容是否包含 <AppContainer 的正则(比完整 AST 解析更快)
|
|
46
|
+
const APP_CONTAINER_PATTERN = /<AppContainer[\s/>]/;
|
|
47
|
+
/**
|
|
48
|
+
* 同步检查文件是否包含 AppContainer 使用
|
|
49
|
+
* 使用正则匹配,性能优于完整 AST 解析
|
|
50
|
+
*/
|
|
51
|
+
function checkFileForAppContainer(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
if (!fs.existsSync(filePath)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
57
|
+
return APP_CONTAINER_PATTERN.test(content);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// 文件不存在或无法读取,视为没有使用
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
42
64
|
const rule = {
|
|
43
65
|
meta: {
|
|
44
66
|
type: 'problem',
|
|
@@ -68,17 +90,18 @@ const rule = {
|
|
|
68
90
|
const customMessage = options?.message;
|
|
69
91
|
const filename = context.filename || context.getFilename();
|
|
70
92
|
// 获取文件名和目录名
|
|
71
|
-
const basename = path.basename(filename);
|
|
93
|
+
const basename = path.basename(filename).toLowerCase();
|
|
72
94
|
const dirname = path.dirname(filename);
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
95
|
+
// 判断是否在 client/src/ 目录下
|
|
96
|
+
const isInClientSrc = CLIENT_SRC_PATTERN.test(dirname + path.sep);
|
|
97
|
+
// 只在 app.tsx 上执行检查和报错
|
|
98
|
+
// index.tsx 不执行检查(避免重复报错)
|
|
99
|
+
const isPrimaryFile = basename === PRIMARY_FILE && isInClientSrc;
|
|
100
|
+
if (!isPrimaryFile) {
|
|
78
101
|
return {};
|
|
79
102
|
}
|
|
80
|
-
//
|
|
81
|
-
let
|
|
103
|
+
// 标记当前文件是否使用了 AppContainer
|
|
104
|
+
let hasAppContainerInCurrentFile = false;
|
|
82
105
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
106
|
let programNode = null;
|
|
84
107
|
return {
|
|
@@ -89,7 +112,7 @@ const rule = {
|
|
|
89
112
|
const elementName = node.name;
|
|
90
113
|
if (elementName?.type === 'JSXIdentifier' &&
|
|
91
114
|
elementName?.name === 'AppContainer') {
|
|
92
|
-
|
|
115
|
+
hasAppContainerInCurrentFile = true;
|
|
93
116
|
}
|
|
94
117
|
},
|
|
95
118
|
// 保存 Program 节点,用于在文件末尾报错时定位
|
|
@@ -98,7 +121,19 @@ const rule = {
|
|
|
98
121
|
},
|
|
99
122
|
// 文件解析完成后,检查是否在 JSX 中使用了 AppContainer
|
|
100
123
|
'Program:exit'() {
|
|
101
|
-
|
|
124
|
+
// 如果当前文件(app.tsx)有 AppContainer,通过
|
|
125
|
+
if (hasAppContainerInCurrentFile) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// 当前文件没有,检查 index.tsx
|
|
129
|
+
const secondaryFilePath = path.join(dirname, SECONDARY_FILE);
|
|
130
|
+
const hasAppContainerInSecondary = checkFileForAppContainer(secondaryFilePath);
|
|
131
|
+
// 如果 index.tsx 有 AppContainer,也算通过
|
|
132
|
+
if (hasAppContainerInSecondary) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// 两个文件都没有,报错
|
|
136
|
+
if (programNode) {
|
|
102
137
|
context.report({
|
|
103
138
|
node: programNode,
|
|
104
139
|
...(customMessage
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint 规则:使用 scroll-reveal 样式类时必须调用 useScrollReveal hook
|
|
3
|
+
*
|
|
4
|
+
* 此规则确保开发者在使用 scroll-reveal 滚动动画样式时,
|
|
5
|
+
* 正确导入并调用 useScrollReveal hook 来启用动画效果。
|
|
6
|
+
*
|
|
7
|
+
* 错误示例:
|
|
8
|
+
* <div className="scroll-reveal">Content</div> // 未使用 hook
|
|
9
|
+
*
|
|
10
|
+
* 正确示例:
|
|
11
|
+
* import { useScrollReveal } from '@lark-apaas/client-toolkit/hooks/useScrollReveal';
|
|
12
|
+
* useScrollReveal(); // 无需参数,默认监听整个页面
|
|
13
|
+
* return <div className="scroll-reveal">Content</div>
|
|
14
|
+
*/
|
|
15
|
+
import type { Rule } from 'eslint';
|
|
16
|
+
declare const rule: Rule.RuleModule;
|
|
17
|
+
export default rule;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const DEFAULT_MESSAGE = "Elements with 'scroll-reveal' class require the useScrollReveal hook. " +
|
|
4
|
+
"Import and call useScrollReveal() from '@lark-apaas/client-toolkit/hooks/useScrollReveal'.";
|
|
5
|
+
// 匹配 className 中的 scroll-reveal 类
|
|
6
|
+
const SCROLL_REVEAL_PATTERN = /(^|\s)scroll-reveal(\s|$)/;
|
|
7
|
+
// Hook 相关常量
|
|
8
|
+
const HOOK_NAME = 'useScrollReveal';
|
|
9
|
+
const HOOK_IMPORT_SOURCE = '@lark-apaas/client-toolkit/hooks/useScrollReveal';
|
|
10
|
+
/**
|
|
11
|
+
* 从 AST 节点中提取 className 字符串值
|
|
12
|
+
* 支持:字符串字面量、模板字符串、字符串拼接、JSX 表达式容器
|
|
13
|
+
*/
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
function extractClassNameValue(node) {
|
|
16
|
+
if (!node)
|
|
17
|
+
return null;
|
|
18
|
+
switch (node.type) {
|
|
19
|
+
case 'Literal':
|
|
20
|
+
return typeof node.value === 'string' ? node.value : null;
|
|
21
|
+
case 'TemplateLiteral':
|
|
22
|
+
// 拼接所有静态部分(quasis)
|
|
23
|
+
return node.quasis
|
|
24
|
+
.map((q) => q.value.raw)
|
|
25
|
+
.join('');
|
|
26
|
+
case 'BinaryExpression':
|
|
27
|
+
// 处理字符串拼接 'a' + 'b'
|
|
28
|
+
if (node.operator === '+') {
|
|
29
|
+
const left = extractClassNameValue(node.left);
|
|
30
|
+
const right = extractClassNameValue(node.right);
|
|
31
|
+
if (left !== null || right !== null) {
|
|
32
|
+
return (left || '') + (right || '');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
case 'JSXExpressionContainer':
|
|
37
|
+
return extractClassNameValue(node.expression);
|
|
38
|
+
default:
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const rule = {
|
|
43
|
+
meta: {
|
|
44
|
+
type: 'problem',
|
|
45
|
+
docs: {
|
|
46
|
+
description: '使用 scroll-reveal 样式类时必须导入并调用 useScrollReveal hook',
|
|
47
|
+
category: 'Best Practices',
|
|
48
|
+
recommended: true,
|
|
49
|
+
},
|
|
50
|
+
schema: [
|
|
51
|
+
{
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
message: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: '自定义错误信息',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
messages: {
|
|
63
|
+
requireScrollRevealHook: DEFAULT_MESSAGE,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
create(context) {
|
|
67
|
+
const options = context.options[0];
|
|
68
|
+
const customMessage = options?.message;
|
|
69
|
+
// 文件级别的状态追踪
|
|
70
|
+
let hasScrollRevealClass = false;
|
|
71
|
+
let hasHookImport = false;
|
|
72
|
+
let hasHookCall = false;
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
let scrollRevealNodes = []; // 保存有 scroll-reveal 的节点用于报错定位
|
|
75
|
+
return {
|
|
76
|
+
// 检测 JSX 中的 className 属性
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
JSXAttribute(node) {
|
|
79
|
+
if (node.name?.name !== 'className')
|
|
80
|
+
return;
|
|
81
|
+
const classValue = extractClassNameValue(node.value);
|
|
82
|
+
if (classValue && SCROLL_REVEAL_PATTERN.test(classValue)) {
|
|
83
|
+
hasScrollRevealClass = true;
|
|
84
|
+
scrollRevealNodes.push(node);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
// 检测 useScrollReveal 导入
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
ImportDeclaration(node) {
|
|
90
|
+
const source = node.source?.value;
|
|
91
|
+
if (typeof source !== 'string')
|
|
92
|
+
return;
|
|
93
|
+
// 检查是否从正确的源导入
|
|
94
|
+
if (source === HOOK_IMPORT_SOURCE ||
|
|
95
|
+
source.includes('useScrollReveal')) {
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
const hasHookSpecifier = node.specifiers?.some((spec) => {
|
|
98
|
+
if (spec.type === 'ImportSpecifier') {
|
|
99
|
+
return spec.imported?.name === HOOK_NAME;
|
|
100
|
+
}
|
|
101
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
102
|
+
return spec.local?.name === HOOK_NAME;
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
if (hasHookSpecifier) {
|
|
107
|
+
hasHookImport = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
// 检测 useScrollReveal 调用
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
CallExpression(node) {
|
|
114
|
+
if (node.callee?.name === HOOK_NAME) {
|
|
115
|
+
hasHookCall = true;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
// 文件遍历结束时进行检查
|
|
119
|
+
'Program:exit'() {
|
|
120
|
+
// 只有在使用了 scroll-reveal 类但未正确设置 hook 时报错
|
|
121
|
+
if (hasScrollRevealClass && (!hasHookImport || !hasHookCall)) {
|
|
122
|
+
// 在第一个使用 scroll-reveal 的节点上报错,提供更好的开发体验
|
|
123
|
+
const reportNode = scrollRevealNodes[0];
|
|
124
|
+
context.report({
|
|
125
|
+
node: reportNode,
|
|
126
|
+
...(customMessage
|
|
127
|
+
? { message: customMessage }
|
|
128
|
+
: { messageId: 'requireScrollRevealHook' }),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
exports.default = rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自定义 Stylelint 规则:校验 CSS 声明中非法的 hsl() 写法
|
|
3
|
+
*
|
|
4
|
+
* 遍历所有 CSS 声明(包括自定义属性 --*),当值以 hsl()/hsla() 开头时,
|
|
5
|
+
* 使用 css-tree 的词法器验证其是否符合 CSS color 规范。
|
|
6
|
+
*
|
|
7
|
+
* 注意:只处理纯颜色值声明,不处理复合值(如 box-shadow、background 等)中的 hsl()。
|
|
8
|
+
*
|
|
9
|
+
* 非法示例:
|
|
10
|
+
* --color: hsl(100%, 50%, 26%); // hue 不能是百分比
|
|
11
|
+
* --color: hsl(200 50% 26% 0.5); // alpha 缺少斜杠
|
|
12
|
+
* --color: hsl(200 50%); // 缺少 lightness
|
|
13
|
+
* color: hsl(); // 空参数
|
|
14
|
+
*
|
|
15
|
+
* 合法示例:
|
|
16
|
+
* --color: hsl(200, 50%, 26%); // CSS3 逗号写法
|
|
17
|
+
* --color: hsl(200 50% 26%); // CSS4 空格写法
|
|
18
|
+
* --color: hsl(200deg 50% 26%); // 带单位的 hue
|
|
19
|
+
* --color: hsl(200 50% 26% / 0.5); // CSS4 带 alpha
|
|
20
|
+
*
|
|
21
|
+
* 跳过(不检查)的示例:
|
|
22
|
+
* --shadow: 0px 2px 12px hsl(0 0% 0% / 0.01); // 复合值中的 hsl
|
|
23
|
+
* background: linear-gradient(hsl(...), ...); // 复合值中的 hsl
|
|
24
|
+
* --border: hsl(from hsl(217 80% 55%) h s calc(l + var(--x))); // CSS Color Level 5 相对颜色语法
|
|
25
|
+
* --color: hsl(none 50% 26%); // none 关键字(CSS Color Level 4)
|
|
26
|
+
*
|
|
27
|
+
* var() 通道的处理:
|
|
28
|
+
* 对每个 var() 独立尝试占位符 0 和 0%(hue 位置需要数字,sat/light 位置需要百分比),
|
|
29
|
+
* 穷举所有 2^n 组合,只要存在一种合法结构就认为该写法合法。
|
|
30
|
+
* 这样可以在不误报合法写法的前提下,仍然捕获结构性错误(如缺少参数)。
|
|
31
|
+
*/
|
|
32
|
+
import stylelint from 'stylelint';
|
|
33
|
+
export declare const ruleName = "custom/hsl-valid-value";
|
|
34
|
+
export declare const messages: {
|
|
35
|
+
invalidHsl: (value: string) => string;
|
|
36
|
+
};
|
|
37
|
+
export declare const meta: {
|
|
38
|
+
url: string;
|
|
39
|
+
};
|
|
40
|
+
declare const _default: stylelint.Plugin;
|
|
41
|
+
export default _default;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.meta = exports.messages = exports.ruleName = void 0;
|
|
40
|
+
/**
|
|
41
|
+
* 自定义 Stylelint 规则:校验 CSS 声明中非法的 hsl() 写法
|
|
42
|
+
*
|
|
43
|
+
* 遍历所有 CSS 声明(包括自定义属性 --*),当值以 hsl()/hsla() 开头时,
|
|
44
|
+
* 使用 css-tree 的词法器验证其是否符合 CSS color 规范。
|
|
45
|
+
*
|
|
46
|
+
* 注意:只处理纯颜色值声明,不处理复合值(如 box-shadow、background 等)中的 hsl()。
|
|
47
|
+
*
|
|
48
|
+
* 非法示例:
|
|
49
|
+
* --color: hsl(100%, 50%, 26%); // hue 不能是百分比
|
|
50
|
+
* --color: hsl(200 50% 26% 0.5); // alpha 缺少斜杠
|
|
51
|
+
* --color: hsl(200 50%); // 缺少 lightness
|
|
52
|
+
* color: hsl(); // 空参数
|
|
53
|
+
*
|
|
54
|
+
* 合法示例:
|
|
55
|
+
* --color: hsl(200, 50%, 26%); // CSS3 逗号写法
|
|
56
|
+
* --color: hsl(200 50% 26%); // CSS4 空格写法
|
|
57
|
+
* --color: hsl(200deg 50% 26%); // 带单位的 hue
|
|
58
|
+
* --color: hsl(200 50% 26% / 0.5); // CSS4 带 alpha
|
|
59
|
+
*
|
|
60
|
+
* 跳过(不检查)的示例:
|
|
61
|
+
* --shadow: 0px 2px 12px hsl(0 0% 0% / 0.01); // 复合值中的 hsl
|
|
62
|
+
* background: linear-gradient(hsl(...), ...); // 复合值中的 hsl
|
|
63
|
+
* --border: hsl(from hsl(217 80% 55%) h s calc(l + var(--x))); // CSS Color Level 5 相对颜色语法
|
|
64
|
+
* --color: hsl(none 50% 26%); // none 关键字(CSS Color Level 4)
|
|
65
|
+
*
|
|
66
|
+
* var() 通道的处理:
|
|
67
|
+
* 对每个 var() 独立尝试占位符 0 和 0%(hue 位置需要数字,sat/light 位置需要百分比),
|
|
68
|
+
* 穷举所有 2^n 组合,只要存在一种合法结构就认为该写法合法。
|
|
69
|
+
* 这样可以在不误报合法写法的前提下,仍然捕获结构性错误(如缺少参数)。
|
|
70
|
+
*/
|
|
71
|
+
const stylelint_1 = __importDefault(require("stylelint"));
|
|
72
|
+
const csstree = __importStar(require("css-tree"));
|
|
73
|
+
exports.ruleName = 'custom/hsl-valid-value';
|
|
74
|
+
const { report, ruleMessages, validateOptions } = stylelint_1.default.utils;
|
|
75
|
+
exports.messages = ruleMessages(exports.ruleName, {
|
|
76
|
+
invalidHsl: (value) => `Invalid hsl() value: "${value}". Refer to https://developer.mozilla.org/docs/Web/CSS/color_value/hsl`,
|
|
77
|
+
});
|
|
78
|
+
exports.meta = {
|
|
79
|
+
url: 'https://github.com/csstree/stylelint-validator',
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* 提取字符串中所有顶层 var() 的位置和长度,使用括号计数支持任意深度嵌套。
|
|
83
|
+
* 例如 var(--a, var(--b, var(--c, 0))) 会被作为一个整体提取。
|
|
84
|
+
*/
|
|
85
|
+
function extractVarRanges(value) {
|
|
86
|
+
const ranges = [];
|
|
87
|
+
const varStart = /var\s*\(/gi;
|
|
88
|
+
let m;
|
|
89
|
+
while ((m = varStart.exec(value)) !== null) {
|
|
90
|
+
let depth = 1;
|
|
91
|
+
let j = m.index + m[0].length;
|
|
92
|
+
while (j < value.length && depth > 0) {
|
|
93
|
+
if (value[j] === '(')
|
|
94
|
+
depth++;
|
|
95
|
+
else if (value[j] === ')')
|
|
96
|
+
depth--;
|
|
97
|
+
j++;
|
|
98
|
+
}
|
|
99
|
+
ranges.push({ index: m.index, length: j - m.index });
|
|
100
|
+
varStart.lastIndex = j; // 跳过已处理的 var(),避免重复匹配内层
|
|
101
|
+
}
|
|
102
|
+
return ranges;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 对 hsl() 值中的每个 var() 分别尝试占位符 "0" 和 "0%",
|
|
106
|
+
* 穷举 2^n 种组合,只要任意一种通过 css-tree 词法验证就返回 true。
|
|
107
|
+
*
|
|
108
|
+
* 背景:css-tree 不能解析 var(),但不同通道对占位符类型要求不同:
|
|
109
|
+
* - hue 位置需要数字(0),saturation/lightness 位置需要百分比(0%)。
|
|
110
|
+
* 穷举组合避免了需要感知通道位置的复杂解析。
|
|
111
|
+
*/
|
|
112
|
+
function isHslValidWithVarPlaceholders(value, ranges) {
|
|
113
|
+
const isValidCssColor = (v) => {
|
|
114
|
+
try {
|
|
115
|
+
const ast = csstree.parse(v, { context: 'value' });
|
|
116
|
+
return !csstree.lexer.matchType('color', ast).error;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const n = ranges.length;
|
|
123
|
+
const total = 1 << n; // 2^n 种组合
|
|
124
|
+
for (let mask = 0; mask < total; mask++) {
|
|
125
|
+
let replaced = value;
|
|
126
|
+
let offset = 0;
|
|
127
|
+
for (let i = 0; i < n; i++) {
|
|
128
|
+
const placeholder = (mask >> i) & 1 ? '0%' : '0';
|
|
129
|
+
const m = ranges[i];
|
|
130
|
+
const start = m.index + offset;
|
|
131
|
+
const end = start + m.length;
|
|
132
|
+
replaced = replaced.slice(0, start) + placeholder + replaced.slice(end);
|
|
133
|
+
offset += placeholder.length - m.length;
|
|
134
|
+
}
|
|
135
|
+
if (isValidCssColor(replaced))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false; // 所有组合均非法,视为结构性错误
|
|
139
|
+
}
|
|
140
|
+
const rule = primary => {
|
|
141
|
+
return (root, result) => {
|
|
142
|
+
const validOptions = validateOptions(result, exports.ruleName, {
|
|
143
|
+
actual: primary,
|
|
144
|
+
possible: [true],
|
|
145
|
+
});
|
|
146
|
+
if (!validOptions || !primary)
|
|
147
|
+
return;
|
|
148
|
+
root.walkDecls(decl => {
|
|
149
|
+
const value = decl.value.trim();
|
|
150
|
+
// 只处理纯 hsl/hsla 颜色值,跳过复合值(如 box-shadow)
|
|
151
|
+
if (!/^hsla?\(/i.test(value))
|
|
152
|
+
return;
|
|
153
|
+
// 跳过 CSS Color Level 5 相对颜色语法:hsl(from <color> h s l)
|
|
154
|
+
// css-tree 词法器尚不支持该语法,直接跳过避免误报
|
|
155
|
+
if (/^hsla?\(\s*from\s+/i.test(value))
|
|
156
|
+
return;
|
|
157
|
+
// none 关键字只在现代空格语法中合法(CSS Color Level 4),逗号语法不支持
|
|
158
|
+
// 通过是否含逗号区分语法:现代语法跳过,逗号语法交由 css-tree 验证
|
|
159
|
+
const isLegacySyntax = /^hsla?\s*\([^)]*,/i.test(value);
|
|
160
|
+
if (!isLegacySyntax && /\bnone\b/i.test(value))
|
|
161
|
+
return;
|
|
162
|
+
// 包含 var() 时,穷举占位符组合进行结构性校验
|
|
163
|
+
const varRanges = extractVarRanges(value);
|
|
164
|
+
if (varRanges.length > 0) {
|
|
165
|
+
if (!isHslValidWithVarPlaceholders(value, varRanges)) {
|
|
166
|
+
report({
|
|
167
|
+
message: exports.messages.invalidHsl(value),
|
|
168
|
+
node: decl,
|
|
169
|
+
result,
|
|
170
|
+
ruleName: exports.ruleName,
|
|
171
|
+
word: value,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
const ast = csstree.parse(value, { context: 'value' });
|
|
178
|
+
const matchResult = csstree.lexer.matchType('color', ast);
|
|
179
|
+
if (matchResult.error) {
|
|
180
|
+
report({
|
|
181
|
+
message: exports.messages.invalidHsl(value),
|
|
182
|
+
node: decl,
|
|
183
|
+
result,
|
|
184
|
+
ruleName: exports.ruleName,
|
|
185
|
+
word: value,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// css-tree parse 失败也视为非法值
|
|
191
|
+
report({
|
|
192
|
+
message: exports.messages.invalidHsl(value),
|
|
193
|
+
node: decl,
|
|
194
|
+
result,
|
|
195
|
+
ruleName: exports.ruleName,
|
|
196
|
+
word: value,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
rule.ruleName = exports.ruleName;
|
|
203
|
+
rule.messages = exports.messages;
|
|
204
|
+
rule.meta = exports.meta;
|
|
205
|
+
exports.default = stylelint_1.default.createPlugin(exports.ruleName, rule);
|
|
@@ -17,6 +17,8 @@ declare const _default: {
|
|
|
17
17
|
rules: {
|
|
18
18
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
19
19
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
20
|
+
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
21
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
20
22
|
};
|
|
21
23
|
};
|
|
22
24
|
};
|
|
@@ -23,9 +23,7 @@ exports.default = {
|
|
|
23
23
|
plugins: {
|
|
24
24
|
'react-hooks': reactHooks,
|
|
25
25
|
import: importPlugin,
|
|
26
|
-
'@lark-apaas': {
|
|
27
|
-
rules: custom_eslint_rules_1.customRules,
|
|
28
|
-
},
|
|
26
|
+
'@lark-apaas': { rules: custom_eslint_rules_1.customRules },
|
|
29
27
|
},
|
|
30
28
|
settings: {
|
|
31
29
|
'import/resolver': {
|
|
@@ -42,6 +40,8 @@ exports.default = {
|
|
|
42
40
|
...reactHooks.configs.recommended.rules,
|
|
43
41
|
// 平台规则:确保入口文件包含 AppContainer 组件
|
|
44
42
|
'@lark-apaas/require-app-container': 'error',
|
|
43
|
+
// 平台规则:禁止直接调用 capability 内部 API,应使用 capabilityClient
|
|
44
|
+
'@lark-apaas/no-direct-capability-api': 'error',
|
|
45
45
|
// TypeScript 规则
|
|
46
46
|
'@typescript-eslint/no-unused-vars': 'off', // 未使用变量检查关闭
|
|
47
47
|
'@typescript-eslint/no-explicit-any': 'off', // 允许使用 any 类型
|
|
@@ -56,8 +56,10 @@ exports.default = {
|
|
|
56
56
|
'no-undef': 'off', // 禁止使用未声明的变量
|
|
57
57
|
'no-console': 'off', // 允许使用 console
|
|
58
58
|
'prefer-const': 'off', // 不强制使用 const
|
|
59
|
+
'no-case-declarations': 'off', // 允许在 case 块中使用词法声明
|
|
60
|
+
'no-empty': 'off', // 允许空的 catch 块
|
|
59
61
|
// Import 规则
|
|
60
|
-
'import/no-unresolved': 'error', //
|
|
62
|
+
'import/no-unresolved': ['error', { ignore: ['\\?raw$'] }], // 检查导入路径是否存在,忽略 ?raw 查询参数
|
|
61
63
|
// 其他规则
|
|
62
64
|
'no-constant-binary-expression': 'off', // 不强制使用常量二进制表达式
|
|
63
65
|
// React Refresh 相关 - 开发时不影响渲染的检测
|
|
@@ -112,34 +114,18 @@ exports.default = {
|
|
|
112
114
|
selector: 'AssignmentExpression[left.object.name="location"][left.property.name="href"]',
|
|
113
115
|
message: "Please don't use `location.href` to navigate. Use `useNavigate` hook from 'react-router-dom' instead.",
|
|
114
116
|
},
|
|
115
|
-
// SelectItem组件的value属性值不能为空字符串
|
|
116
|
-
{
|
|
117
|
-
message: 'The `value` attribute of the `SelectItem` component cannot be an empty string.',
|
|
118
|
-
selector: 'JSXOpeningElement[name.name="SelectItem"]:has(JSXAttribute[name.name="value"][value.value=""]), JSXOpeningElement[name.name="SelectItem"]:has(JSXAttribute[name.name="value"][value.expression.value=""])',
|
|
119
|
-
},
|
|
120
117
|
// 禁止a标签href使用相对路径
|
|
121
118
|
{
|
|
122
119
|
selector: "JSXOpeningElement[name.name='a']:has(JSXAttribute[name.name='href'][value.value=/^(?!https?:|\\u002F\\u002F|mailto:|tel:|#).+/])",
|
|
123
120
|
message: "Please don't use relative paths in <a> tags. Use NavLink from 'react-router-dom' instead.",
|
|
124
121
|
},
|
|
125
|
-
// 禁止 variant 为 outline|link|ghost 的 Button 使用 text-white
|
|
126
|
-
{
|
|
127
|
-
selector: 'JSXElement[openingElement.name.name="Button"]' +
|
|
128
|
-
':has(JSXAttribute[name.name="variant"][value.value=/^(outline|link|ghost)$/])' +
|
|
129
|
-
':has(JSXAttribute[name.name="className"][value.value=/text-white/])',
|
|
130
|
-
message: 'Button with variant="outline|link|ghost" should not use "text-white" className. This causes visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
131
|
-
},
|
|
132
|
-
// 禁止在 Button 上组合 text-primary-foreground 与 bg-background 并用的情况
|
|
133
122
|
{
|
|
134
|
-
selector: '
|
|
135
|
-
|
|
136
|
-
':has(JSXAttribute[name.name="className"][value.value=/bg-background/])',
|
|
137
|
-
message: 'Button should not use "text-primary-foreground" and "bg-background" className. This causes visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
123
|
+
selector: 'JSXAttribute[name.name="className"][value.value=/\\[hsl\\([^\\]]*\\s[^\\]]*\\)/]',
|
|
124
|
+
message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in hsl() values. Example: from-[hsl(215_60%_18%)] instead of from-[hsl(215 60% 18%)]',
|
|
138
125
|
},
|
|
139
|
-
// 禁止使用 text-accent
|
|
140
126
|
{
|
|
141
|
-
selector: 'JSXAttribute[name.name="className"][value.value
|
|
142
|
-
message: '
|
|
127
|
+
selector: 'JSXAttribute[name.name="className"][value.value=/\\[rgb\\([^\\]]*\\s[^\\]]*\\)/]',
|
|
128
|
+
message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in rgb() values. Example: bg-[rgb(255_255_255)] instead of bg-[rgb(255 255 255)]',
|
|
143
129
|
},
|
|
144
130
|
],
|
|
145
131
|
},
|
|
@@ -20,6 +20,8 @@ export declare const eslintPresets: {
|
|
|
20
20
|
rules: {
|
|
21
21
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
22
22
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
23
|
+
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
24
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
23
25
|
};
|
|
24
26
|
};
|
|
25
27
|
};
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.stylelintPresets = void 0;
|
|
37
|
+
const hsl_variable_1 = __importStar(require("../../custom-stylelint-rules/hsl-variable"));
|
|
38
|
+
const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
|
|
4
39
|
exports.stylelintPresets = {
|
|
40
|
+
plugins: [hsl_variable_1.default],
|
|
5
41
|
rules: {
|
|
6
42
|
'declaration-block-no-duplicate-custom-properties': true,
|
|
43
|
+
[hsl_variable_1.ruleName]: isLooseMode || null,
|
|
7
44
|
},
|
|
8
45
|
};
|
|
@@ -15,14 +15,16 @@ declare const _default: ({
|
|
|
15
15
|
globals: any;
|
|
16
16
|
};
|
|
17
17
|
plugins: {
|
|
18
|
-
'react-hooks': any;
|
|
19
|
-
import: any;
|
|
20
18
|
'@lark-apaas': {
|
|
21
19
|
rules: {
|
|
22
20
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
23
21
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
22
|
+
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
23
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
|
+
'react-hooks': any;
|
|
27
|
+
import: any;
|
|
26
28
|
};
|
|
27
29
|
settings: {
|
|
28
30
|
'import/resolver': {
|
|
@@ -40,8 +42,8 @@ declare const _default: ({
|
|
|
40
42
|
files: string[];
|
|
41
43
|
rules: {
|
|
42
44
|
'no-restricted-syntax': (string | {
|
|
43
|
-
message: string;
|
|
44
45
|
selector: string;
|
|
46
|
+
message: string;
|
|
45
47
|
})[];
|
|
46
48
|
};
|
|
47
49
|
} | null)[];
|
|
@@ -5,16 +5,29 @@ const globals = require('globals');
|
|
|
5
5
|
const reactHooks = require('eslint-plugin-react-hooks');
|
|
6
6
|
const tseslint = require('typescript-eslint');
|
|
7
7
|
const importPlugin = require('eslint-plugin-import');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
8
10
|
const custom_eslint_rules_1 = require("../../../custom-eslint-rules");
|
|
9
11
|
// 检查是否启用宽松 lint 模式
|
|
10
12
|
const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
|
|
13
|
+
// 读取项目 package.json 中的 flags 配置
|
|
14
|
+
function getProjectFlags() {
|
|
15
|
+
try {
|
|
16
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
17
|
+
if (fs.existsSync(pkgPath)) {
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
return pkg.flags || {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// ignore errors
|
|
24
|
+
}
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const projectFlags = getProjectFlags();
|
|
28
|
+
const supportScrollReveal = projectFlags.supportScrollReveal === true;
|
|
11
29
|
// 基础语法规则:所有模式都启用(包括 loose 模式)
|
|
12
30
|
const baseSyntaxRules = [
|
|
13
|
-
// SelectItem组件的value属性值不能为空字符串
|
|
14
|
-
{
|
|
15
|
-
message: 'The `value` attribute of the `SelectItem` component cannot be an empty string.',
|
|
16
|
-
selector: 'JSXOpeningElement[name.name="SelectItem"]:has(JSXAttribute[name.name="value"][value.value=""]), JSXOpeningElement[name.name="SelectItem"]:has(JSXAttribute[name.name="value"][value.expression.value=""])',
|
|
17
|
-
},
|
|
18
31
|
// 禁用`window.location.href`赋值使用,可以读取
|
|
19
32
|
{
|
|
20
33
|
selector: 'AssignmentExpression[left.object.object.name="window"][left.object.property.name="location"][left.property.name="href"]',
|
|
@@ -44,6 +57,32 @@ exports.looseRestrictSyntaxRules = [
|
|
|
44
57
|
selector: "VariableDeclarator[id.name='RoutesComponent'] > ArrowFunctionExpression > JSXElement:not([openingElement.name.name='Routes'])",
|
|
45
58
|
message: 'RoutesComponent must return <Routes> directly without any wrapper. Do not add <div>, <BrowserRouter> or other wrappers. You can only modify routes inside <Routes> (add/edit/remove <Route> elements).',
|
|
46
59
|
},
|
|
60
|
+
// 禁止在 Tailwind 任意值语法中使用包含空格的 hsl/rgb 值
|
|
61
|
+
{
|
|
62
|
+
selector: 'JSXAttribute[name.name="className"][value.value=/\\[hsl\\([^\\]]*\\s[^\\]]*\\)/]',
|
|
63
|
+
message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in hsl() values. Example: from-[hsl(215_60%_18%)] instead of from-[hsl(215 60% 18%)]',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
selector: 'JSXAttribute[name.name="className"][value.value=/\\[rgb\\([^\\]]*\\s[^\\]]*\\)/]',
|
|
67
|
+
message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in rgb() values. Example: bg-[rgb(255_255_255)] instead of bg-[rgb(255 255 255)]',
|
|
68
|
+
},
|
|
69
|
+
// 阻止模型引用 @shared/static 与 shared/static 的字符串
|
|
70
|
+
{
|
|
71
|
+
selector: 'Literal[value=/^@shared\\u002Fstatic/]:not(ImportDeclaration > Literal):not(ExportAllDeclaration > Literal):not(ExportNamedDeclaration > Literal):not(ImportExpression > Literal)',
|
|
72
|
+
message: "Do not use '@shared/static/*' or 'shared/static/*' as a raw string (e.g. in fetch, image src, etc.). Shared static assets must be accessed via import statements.",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
selector: 'TemplateLiteral[expressions.length=0] > TemplateElement[value.raw=/^@shared\\u002Fstatic/]',
|
|
76
|
+
message: "Do not use '@shared/static/*' or 'shared/static/*' as a raw string (e.g. in fetch, image src, etc.). Shared static assets must be accessed via import statements.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
selector: 'Literal[value=/^shared\\u002Fstatic/]:not(ImportDeclaration > Literal):not(ExportAllDeclaration > Literal):not(ExportNamedDeclaration > Literal):not(ImportExpression > Literal)',
|
|
80
|
+
message: "Do not use '@shared/static/*' or 'shared/static/*' as a raw string (e.g. in fetch, image src, etc.). Shared static assets must be accessed via import statements.",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
selector: 'TemplateLiteral[expressions.length=0] > TemplateElement[value.raw=/^shared\\u002Fstatic/]',
|
|
84
|
+
message: "Do not use '@shared/static/*' or 'shared/static/*' as a raw string (e.g. in fetch, image src, etc.). Shared static assets must be accessed via import statements.",
|
|
85
|
+
},
|
|
47
86
|
];
|
|
48
87
|
// 严格语法规则:仅正常模式启用,loose 模式下不启用
|
|
49
88
|
const strictSyntaxRules = [
|
|
@@ -74,36 +113,20 @@ const strictSyntaxRules = [
|
|
|
74
113
|
message: "Please don't use confirm. It may conflict with window.confirm BOM method, use `Dialog` component instead for better user experience and consistency",
|
|
75
114
|
selector: "CallExpression[callee.name='confirm']",
|
|
76
115
|
},
|
|
77
|
-
// 禁止 variant 为 outline|link|ghost 的 Button 使用 text-white
|
|
78
|
-
{
|
|
79
|
-
selector: 'JSXElement[openingElement.name.name="Button"]' +
|
|
80
|
-
':has(JSXAttribute[name.name="variant"][value.value=/^(outline|link|ghost)$/])' +
|
|
81
|
-
':has(JSXAttribute[name.name="className"][value.value=/text-white/])',
|
|
82
|
-
message: 'Button with variant="outline|link|ghost" should not use "text-white" className. This causes visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
83
|
-
},
|
|
84
|
-
// 禁止在 Button 上组合 text-primary-foreground 与 bg-background 并用的情况
|
|
85
|
-
{
|
|
86
|
-
selector: 'JSXElement[openingElement.name.name="Button"]' +
|
|
87
|
-
':has(JSXAttribute[name.name="className"][value.value=/text-primary-foreground/])' +
|
|
88
|
-
':has(JSXAttribute[name.name="className"][value.value=/bg-background/])',
|
|
89
|
-
message: 'Button should not use "text-primary-foreground" and "bg-background" className. This causes visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
90
|
-
},
|
|
91
|
-
// 禁止使用 text-accent
|
|
92
|
-
{
|
|
93
|
-
selector: 'JSXAttribute[name.name="className"][value.value=/(^|\\s)text-accent(\\s|$)/]',
|
|
94
|
-
message: 'Classname "text-accent" would cause visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
95
|
-
},
|
|
96
116
|
];
|
|
97
|
-
const
|
|
98
|
-
'@lark-apaas
|
|
117
|
+
const looseSpecificPlugins = {
|
|
118
|
+
'@lark-apaas': { rules: custom_eslint_rules_1.customRules },
|
|
99
119
|
};
|
|
100
|
-
//
|
|
120
|
+
// 宽松模式下覆盖的规则
|
|
101
121
|
const looseOverrideRules = isLooseMode
|
|
102
122
|
? {
|
|
103
|
-
...looseSpecificRules,
|
|
104
123
|
'@typescript-eslint/no-unsafe-function-type': 'off',
|
|
105
124
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
106
125
|
'no-useless-escape': 'off', // 允许不必要的转义字符
|
|
126
|
+
// 使用 scroll-reveal 样式类时必须调用 useScrollReveal()(需 flags.supportScrollReveal 为 true)
|
|
127
|
+
'@lark-apaas/require-scroll-reveal-hook': supportScrollReveal
|
|
128
|
+
? 'error'
|
|
129
|
+
: 'off',
|
|
107
130
|
}
|
|
108
131
|
: {};
|
|
109
132
|
// 基础配置(适用于所有文件)
|
|
@@ -125,7 +148,7 @@ const baseConfig = {
|
|
|
125
148
|
plugins: {
|
|
126
149
|
'react-hooks': reactHooks,
|
|
127
150
|
import: importPlugin,
|
|
128
|
-
|
|
151
|
+
...looseSpecificPlugins,
|
|
129
152
|
},
|
|
130
153
|
settings: {
|
|
131
154
|
'import/resolver': {
|
|
@@ -142,6 +165,9 @@ const baseConfig = {
|
|
|
142
165
|
...reactHooks.configs.recommended.rules,
|
|
143
166
|
// 平台规则:确保入口文件包含 AppContainer 组件
|
|
144
167
|
'@lark-apaas/require-app-container': 'error',
|
|
168
|
+
// 平台规则:禁止直接调用 capability 内部 API,应使用 capabilityClient
|
|
169
|
+
'@lark-apaas/no-direct-capability-api': 'error',
|
|
170
|
+
'@lark-apaas/no-nested-styled-jsx': 'error',
|
|
145
171
|
// TypeScript 规则
|
|
146
172
|
'@typescript-eslint/no-unused-vars': 'off', // 未使用变量检查关闭
|
|
147
173
|
'@typescript-eslint/no-explicit-any': 'off', // 允许使用 any 类型
|
|
@@ -156,8 +182,11 @@ const baseConfig = {
|
|
|
156
182
|
'no-undef': 'off', // 禁止使用未声明的变量
|
|
157
183
|
'no-console': 'off', // 允许使用 console
|
|
158
184
|
'prefer-const': 'off', // 不强制使用 const
|
|
185
|
+
'no-control-regex': 'off',
|
|
186
|
+
'no-useless-escape': 'off',
|
|
187
|
+
'no-case-declarations': 'off',
|
|
159
188
|
// Import 规则
|
|
160
|
-
'import/no-unresolved': 'error', //
|
|
189
|
+
'import/no-unresolved': ['error', { ignore: ['\\?raw$'] }], // 检查导入路径是否存在,忽略 ?raw 查询参数
|
|
161
190
|
// 其他规则
|
|
162
191
|
'no-constant-binary-expression': 'off', // 不强制使用常量二进制表达式
|
|
163
192
|
// React Refresh 相关 - 开发时不影响渲染的检测
|
|
@@ -190,7 +219,11 @@ const appTsxConfig = isLooseMode
|
|
|
190
219
|
name: '@lark-apaas/client-recommend/app-tsx',
|
|
191
220
|
files: ['client/src/app.tsx'],
|
|
192
221
|
rules: {
|
|
193
|
-
'no-restricted-syntax': [
|
|
222
|
+
'no-restricted-syntax': [
|
|
223
|
+
'error',
|
|
224
|
+
...baseSyntaxRules,
|
|
225
|
+
...exports.looseRestrictSyntaxRules,
|
|
226
|
+
],
|
|
194
227
|
},
|
|
195
228
|
}
|
|
196
229
|
: null;
|
|
@@ -29,6 +29,9 @@ declare const _default: {
|
|
|
29
29
|
'no-undef': string;
|
|
30
30
|
'no-console': string;
|
|
31
31
|
'prefer-const': string;
|
|
32
|
+
'no-control-regex': string;
|
|
33
|
+
'no-useless-escape': string;
|
|
34
|
+
'no-case-declarations': string;
|
|
32
35
|
'import/no-unresolved': string;
|
|
33
36
|
'import/no-extraneous-dependencies': string;
|
|
34
37
|
'no-restricted-imports': (string | {
|
|
@@ -39,6 +39,9 @@ exports.default = {
|
|
|
39
39
|
'no-undef': 'off',
|
|
40
40
|
'no-console': 'error',
|
|
41
41
|
'prefer-const': 'off',
|
|
42
|
+
'no-control-regex': 'off',
|
|
43
|
+
'no-useless-escape': 'off',
|
|
44
|
+
'no-case-declarations': 'off',
|
|
42
45
|
// Import 规则
|
|
43
46
|
'import/no-unresolved': 'error',
|
|
44
47
|
'import/no-extraneous-dependencies': 'error',
|
|
@@ -14,14 +14,16 @@ export declare const eslintPresets: {
|
|
|
14
14
|
globals: any;
|
|
15
15
|
};
|
|
16
16
|
plugins: {
|
|
17
|
-
'react-hooks': any;
|
|
18
|
-
import: any;
|
|
19
17
|
'@lark-apaas': {
|
|
20
18
|
rules: {
|
|
21
19
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
22
20
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
21
|
+
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
22
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
+
'react-hooks': any;
|
|
26
|
+
import: any;
|
|
25
27
|
};
|
|
26
28
|
settings: {
|
|
27
29
|
'import/resolver': {
|
|
@@ -39,8 +41,8 @@ export declare const eslintPresets: {
|
|
|
39
41
|
files: string[];
|
|
40
42
|
rules: {
|
|
41
43
|
'no-restricted-syntax': (string | {
|
|
42
|
-
message: string;
|
|
43
44
|
selector: string;
|
|
45
|
+
message: string;
|
|
44
46
|
})[];
|
|
45
47
|
};
|
|
46
48
|
} | null)[] | {
|
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.stylelintPresets = void 0;
|
|
37
|
+
const hsl_variable_1 = __importStar(require("../../../custom-stylelint-rules/hsl-variable"));
|
|
38
|
+
const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
|
|
4
39
|
exports.stylelintPresets = {
|
|
40
|
+
plugins: [hsl_variable_1.default],
|
|
5
41
|
rules: {
|
|
6
42
|
'declaration-block-no-duplicate-custom-properties': true,
|
|
43
|
+
[hsl_variable_1.ruleName]: isLooseMode || null,
|
|
7
44
|
},
|
|
8
45
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-apaas/fullstack-presets",
|
|
3
|
-
"version": "1.1.5-alpha.
|
|
3
|
+
"version": "1.1.5-alpha.21",
|
|
4
4
|
"files": [
|
|
5
5
|
"lib"
|
|
6
6
|
],
|
|
@@ -26,21 +26,25 @@
|
|
|
26
26
|
"@eslint/js": "^9.35.0",
|
|
27
27
|
"@types/node": "^22.19.1",
|
|
28
28
|
"@types/styled-jsx": "^2.2.9",
|
|
29
|
+
"css-tree": "^3.1.0",
|
|
29
30
|
"eslint-import-resolver-alias": "^1.1.2",
|
|
30
31
|
"eslint-plugin-import": "^2.32.0",
|
|
31
32
|
"eslint-plugin-react": "^7.37.5",
|
|
32
33
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
33
34
|
"globals": "^16.4.0",
|
|
35
|
+
"stylelint": "^17.3.0",
|
|
34
36
|
"tailwindcss-animate": "^1.0.7"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@babel/core": "^7.24.0",
|
|
38
40
|
"@babel/preset-react": "^7.24.0",
|
|
41
|
+
"@types/css-tree": "^2.3.11",
|
|
39
42
|
"@types/eslint": "^9.6.0",
|
|
40
43
|
"eslint": "^9.35.0",
|
|
41
44
|
"styled-jsx": "^5.1.6",
|
|
42
45
|
"typescript": "^5.9.2",
|
|
43
|
-
"typescript-eslint": "^8.44.0"
|
|
46
|
+
"typescript-eslint": "^8.44.0",
|
|
47
|
+
"vitest": "^2.0.0"
|
|
44
48
|
},
|
|
45
49
|
"peerDependencies": {
|
|
46
50
|
"eslint": "^9.0.0",
|