@lark-apaas/fullstack-presets 1.1.7 → 1.1.9
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 +1 -0
- package/lib/custom-eslint-rules/index.js +2 -0
- 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/recommend/eslint/eslint-client.d.ts +1 -0
- package/lib/recommend/eslint/eslint-client.js +26 -23
- package/lib/recommend/eslint/index.d.ts +1 -0
- package/lib/simple/recommend/eslint/eslint-client.d.ts +1 -0
- package/lib/simple/recommend/eslint/eslint-client.js +23 -1
- package/lib/simple/recommend/eslint/index.d.ts +1 -0
- package/package.json +1 -1
|
@@ -2,4 +2,5 @@ export declare const customRules: {
|
|
|
2
2
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
3
3
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
4
4
|
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
5
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
5
6
|
};
|
|
@@ -7,8 +7,10 @@ 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
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"));
|
|
10
11
|
exports.customRules = {
|
|
11
12
|
'no-nested-styled-jsx': no_nested_styled_jsx_1.default,
|
|
12
13
|
'require-app-container': require_app_container_1.default,
|
|
13
14
|
'no-direct-capability-api': no_direct_capability_api_1.default,
|
|
15
|
+
'require-scroll-reveal-hook': require_scroll_reveal_hook_1.default,
|
|
14
16
|
};
|
|
@@ -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;
|
|
@@ -18,6 +18,7 @@ declare const _default: {
|
|
|
18
18
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
19
19
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
20
20
|
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
21
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
};
|
|
@@ -56,6 +56,8 @@ 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
62
|
'import/no-unresolved': 'error', // 检查导入路径是否存在
|
|
61
63
|
// 其他规则
|
|
@@ -112,35 +114,36 @@ 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
122
|
// 禁止 variant 为 outline|link|ghost 的 Button 使用 text-white
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
123
|
+
// {
|
|
124
|
+
// selector:
|
|
125
|
+
// 'JSXElement[openingElement.name.name="Button"]' +
|
|
126
|
+
// ':has(JSXAttribute[name.name="variant"][value.value=/^(outline|link|ghost)$/])' +
|
|
127
|
+
// ':has(JSXAttribute[name.name="className"][value.value=/text-white/])',
|
|
128
|
+
// message:
|
|
129
|
+
// '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`',
|
|
130
|
+
// },
|
|
131
|
+
// // 禁止在 Button 上组合 text-primary-foreground 与 bg-background 并用的情况
|
|
132
|
+
// {
|
|
133
|
+
// selector:
|
|
134
|
+
// 'JSXElement[openingElement.name.name="Button"]' +
|
|
135
|
+
// ':has(JSXAttribute[name.name="className"][value.value=/text-primary-foreground/])' +
|
|
136
|
+
// ':has(JSXAttribute[name.name="className"][value.value=/bg-background/])',
|
|
137
|
+
// message:
|
|
138
|
+
// '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`',
|
|
139
|
+
// },
|
|
140
|
+
// // 禁止使用 text-accent
|
|
141
|
+
// {
|
|
142
|
+
// selector:
|
|
143
|
+
// 'JSXAttribute[name.name="className"][value.value=/(^|\\s)text-accent(\\s|$)/]',
|
|
144
|
+
// message:
|
|
145
|
+
// 'Classname "text-accent" would cause visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
146
|
+
// },
|
|
144
147
|
// 禁止在 Tailwind 任意值语法中使用包含空格的 hsl/rgb 值
|
|
145
148
|
{
|
|
146
149
|
selector: 'JSXAttribute[name.name="className"][value.value=/\\[hsl\\([^\\]]*\\s[^\\]]*\\)/]',
|
|
@@ -21,6 +21,7 @@ export declare const eslintPresets: {
|
|
|
21
21
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
22
22
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
23
23
|
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
24
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
27
|
};
|
|
@@ -20,6 +20,7 @@ declare const _default: ({
|
|
|
20
20
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
21
21
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
22
22
|
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
23
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
23
24
|
};
|
|
24
25
|
};
|
|
25
26
|
'react-hooks': any;
|
|
@@ -5,9 +5,27 @@ 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
31
|
// SelectItem组件的value属性值不能为空字符串
|
|
@@ -106,12 +124,16 @@ const strictSyntaxRules = [
|
|
|
106
124
|
const looseSpecificPlugins = {
|
|
107
125
|
'@lark-apaas': { rules: custom_eslint_rules_1.customRules },
|
|
108
126
|
};
|
|
109
|
-
//
|
|
127
|
+
// 宽松模式下覆盖的规则
|
|
110
128
|
const looseOverrideRules = isLooseMode
|
|
111
129
|
? {
|
|
112
130
|
'@typescript-eslint/no-unsafe-function-type': 'off',
|
|
113
131
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
114
132
|
'no-useless-escape': 'off', // 允许不必要的转义字符
|
|
133
|
+
// 使用 scroll-reveal 样式类时必须调用 useScrollReveal()(需 flags.supportScrollReveal 为 true)
|
|
134
|
+
'@lark-apaas/require-scroll-reveal-hook': supportScrollReveal
|
|
135
|
+
? 'error'
|
|
136
|
+
: 'off',
|
|
115
137
|
}
|
|
116
138
|
: {};
|
|
117
139
|
// 基础配置(适用于所有文件)
|
|
@@ -19,6 +19,7 @@ export declare const eslintPresets: {
|
|
|
19
19
|
'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
|
|
20
20
|
'require-app-container': import("eslint").Rule.RuleModule;
|
|
21
21
|
'no-direct-capability-api': import("eslint").Rule.RuleModule;
|
|
22
|
+
'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
|
|
22
23
|
};
|
|
23
24
|
};
|
|
24
25
|
'react-hooks': any;
|