@lark-apaas/fullstack-presets 1.1.3-beta.1 → 1.1.3-beta.3
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/README.md
CHANGED
|
@@ -41,6 +41,16 @@ export default [
|
|
|
41
41
|
];
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
#### 错误信息
|
|
45
|
+
|
|
46
|
+
当检测到嵌套的 styled-jsx 标签时,规则会提供详细的错误信息,指出与哪一行的外层 styled-jsx 标签冲突:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
17:23 error Detected nested styled-jsx tag. The outer styled-jsx tag is at line 4. Read more: https://nextjs.org/docs/messages/nested-styled-jsx-tags @lark-apaas/no-nested-styled-jsx
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
这样可以快速定位到冲突的外层 styled-jsx 标签,方便修复问题。
|
|
53
|
+
|
|
44
54
|
#### 自定义错误信息
|
|
45
55
|
|
|
46
56
|
可通过配置自定义报错信息:
|
|
@@ -53,7 +63,101 @@ rules: {
|
|
|
53
63
|
}
|
|
54
64
|
```
|
|
55
65
|
|
|
66
|
+
注意:使用自定义错误信息时,将不会显示外层 styled-jsx 标签的行号信息。
|
|
67
|
+
|
|
56
68
|
#### 参考资料
|
|
57
69
|
|
|
58
70
|
- [Next.js 错误说明](https://nextjs.org/docs/messages/nested-styled-jsx-tags)
|
|
59
|
-
- [styled-jsx GitHub Issue #42](https://github.com/vercel/styled-jsx/issues/42)
|
|
71
|
+
- [styled-jsx GitHub Issue #42](https://github.com/vercel/styled-jsx/issues/42)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### 路由守卫规则 (RoutesComponent)
|
|
76
|
+
|
|
77
|
+
保护 `client/src/app.tsx` 中的 RoutesComponent 组件结构,确保路由配置不被意外破坏。
|
|
78
|
+
|
|
79
|
+
#### 规则目的
|
|
80
|
+
|
|
81
|
+
该规则强制 `RoutesComponent` 必须直接返回 `<Routes>` 组件,不允许在外层添加任何包装元素(如 `<div>`、`<BrowserRouter>` 等)。这确保了:
|
|
82
|
+
- ✅ 路由结构保持一致和可预测
|
|
83
|
+
- ✅ 防止开发者误修改核心路由架构
|
|
84
|
+
- ✅ 允许在 `<Routes>` 内部自由添加/修改/删除路由配置
|
|
85
|
+
|
|
86
|
+
#### 生效范围
|
|
87
|
+
|
|
88
|
+
**仅在 loose 模式下对 `client/src/app.tsx` 文件生效**
|
|
89
|
+
|
|
90
|
+
该规则通过环境变量 `FORCE_FRAMEWORK_LINT_LOOSE_MODE=true` 启用,并且只检查项目中的 `client/src/app.tsx` 文件。
|
|
91
|
+
|
|
92
|
+
#### 错误示例
|
|
93
|
+
|
|
94
|
+
❌ **不允许:添加外层包装元素**
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// ❌ 错误:使用 div 包装
|
|
98
|
+
const RoutesComponent = () => (
|
|
99
|
+
<div>
|
|
100
|
+
<Routes>
|
|
101
|
+
<Route path="/" element={<HomePage />} />
|
|
102
|
+
</Routes>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// ❌ 错误:使用 BrowserRouter 包装
|
|
107
|
+
const RoutesComponent = () => {
|
|
108
|
+
return (
|
|
109
|
+
<BrowserRouter>
|
|
110
|
+
<Routes>
|
|
111
|
+
<Route path="/" element={<HomePage />} />
|
|
112
|
+
</Routes>
|
|
113
|
+
</BrowserRouter>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### 正确示例
|
|
119
|
+
|
|
120
|
+
✅ **允许:直接返回 Routes,在内部修改路由**
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
// ✅ 正确:直接返回 Routes
|
|
124
|
+
const RoutesComponent = () => (
|
|
125
|
+
<Routes>
|
|
126
|
+
<Route path="/" element={<HomePage />} />
|
|
127
|
+
<Route path="/about" element={<AboutPage />} />
|
|
128
|
+
<Route path="/users" element={<UsersPage />} />
|
|
129
|
+
<Route path="*" element={<NotFound />} />
|
|
130
|
+
</Routes>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// ✅ 正确:使用嵌套路由
|
|
134
|
+
const RoutesComponent = () => (
|
|
135
|
+
<Routes>
|
|
136
|
+
<Route element={<Layout />}>
|
|
137
|
+
<Route index element={<HomePage />} />
|
|
138
|
+
<Route path="/about" element={<AboutPage />} />
|
|
139
|
+
</Route>
|
|
140
|
+
<Route path="*" element={<NotFound />} />
|
|
141
|
+
</Routes>
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### 错误信息
|
|
146
|
+
|
|
147
|
+
当违反规则时,会看到以下错误信息:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
47:23 error 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). no-restricted-syntax
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
错误信息包含三个层次的指导:
|
|
154
|
+
1. **明确要求**:必须直接返回 `<Routes>`,不能有包装器
|
|
155
|
+
2. **具体禁止**:不要添加 `<div>`、`<BrowserRouter>` 或其他包装器
|
|
156
|
+
3. **允许操作**:可以在 `<Routes>` 内部添加/编辑/删除 `<Route>` 元素
|
|
157
|
+
|
|
158
|
+
#### 使用场景
|
|
159
|
+
|
|
160
|
+
该规则主要用于妙搭等需要保护核心路由结构的场景。在这些场景中:
|
|
161
|
+
- 框架会自动管理路由的外层结构(如 BrowserRouter)
|
|
162
|
+
- 开发者只需关注路由配置本身(路径、组件映射等)
|
|
163
|
+
- 防止开发者误修改导致路由失效
|
|
@@ -24,6 +24,7 @@ const rule = {
|
|
|
24
24
|
],
|
|
25
25
|
messages: {
|
|
26
26
|
nestedStyleJsx: DEFAULT_MESSAGE,
|
|
27
|
+
nestedStyleJsxWithLocation: 'Detected nested styled-jsx tag. The outer styled-jsx tag is at line {{line}}. Read more: https://nextjs.org/docs/messages/nested-styled-jsx-tags',
|
|
27
28
|
},
|
|
28
29
|
},
|
|
29
30
|
create(context) {
|
|
@@ -47,24 +48,67 @@ const rule = {
|
|
|
47
48
|
if (!hasJsxAttribute) {
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
|
-
// Count JSXElement ancestors
|
|
51
|
+
// Count JSXElement ancestors and find the outer styled-jsx tag
|
|
51
52
|
// styled-jsx uses ignoreClosing counter that increments on JSXOpeningElement enter
|
|
52
53
|
// and checks if ignoreClosing > 1 on style tag exit
|
|
53
54
|
// @see https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L222
|
|
54
55
|
let jsxElementCount = 0;
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
let outerStyledJsxNode = null;
|
|
55
58
|
let current = node.parent;
|
|
56
59
|
while (current) {
|
|
57
60
|
if (current.type === 'JSXElement') {
|
|
58
61
|
jsxElementCount++;
|
|
62
|
+
// Check if any sibling or ancestor siblings contain a styled-jsx tag
|
|
63
|
+
if (!outerStyledJsxNode && current.parent) {
|
|
64
|
+
const parentNode = current.parent;
|
|
65
|
+
// Check if parent has children that we can iterate
|
|
66
|
+
const children = parentNode.children || (parentNode.type === 'JSXElement' ? parentNode.children : null);
|
|
67
|
+
if (children && Array.isArray(children)) {
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (child.type === 'JSXElement' && child !== node) {
|
|
71
|
+
const childOpeningElement = child.openingElement;
|
|
72
|
+
if (childOpeningElement) {
|
|
73
|
+
const childElementName = childOpeningElement.name;
|
|
74
|
+
if (childElementName?.type === 'JSXIdentifier' &&
|
|
75
|
+
childElementName?.name === 'style') {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const hasChildJsxAttr = childOpeningElement.attributes?.some((attr) => attr.type === 'JSXAttribute' &&
|
|
78
|
+
attr.name?.type === 'JSXIdentifier' &&
|
|
79
|
+
attr.name?.name === 'jsx');
|
|
80
|
+
if (hasChildJsxAttr) {
|
|
81
|
+
outerStyledJsxNode = child;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
59
90
|
}
|
|
60
91
|
current = current.parent;
|
|
61
92
|
}
|
|
62
93
|
// If there's more than 1 JSXElement ancestor, the style tag is nested
|
|
63
94
|
if (jsxElementCount > 1) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
95
|
+
if (outerStyledJsxNode && outerStyledJsxNode.loc) {
|
|
96
|
+
// Report with location information about the outer styled-jsx tag
|
|
97
|
+
context.report({
|
|
98
|
+
node,
|
|
99
|
+
messageId: 'nestedStyleJsxWithLocation',
|
|
100
|
+
data: {
|
|
101
|
+
line: outerStyledJsxNode.loc.start.line,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// Fallback to the original message if we can't find the outer styled-jsx tag
|
|
107
|
+
context.report({
|
|
108
|
+
node,
|
|
109
|
+
...(customMessage ? { message: customMessage } : { messageId: 'nestedStyleJsx' }),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
68
112
|
}
|
|
69
113
|
},
|
|
70
114
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export declare const
|
|
1
|
+
export declare const looseRestrictSyntaxRules: {
|
|
2
2
|
selector: string;
|
|
3
3
|
message: string;
|
|
4
4
|
}[];
|
|
5
|
-
declare const _default: {
|
|
5
|
+
declare const _default: ({
|
|
6
6
|
name: string;
|
|
7
7
|
languageOptions: {
|
|
8
8
|
ecmaVersion: number;
|
|
@@ -34,5 +34,14 @@ declare const _default: {
|
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
rules: any;
|
|
37
|
-
}
|
|
37
|
+
} | {
|
|
38
|
+
name: string;
|
|
39
|
+
files: string[];
|
|
40
|
+
rules: {
|
|
41
|
+
'no-restricted-syntax': (string | {
|
|
42
|
+
message: string;
|
|
43
|
+
selector: string;
|
|
44
|
+
})[];
|
|
45
|
+
};
|
|
46
|
+
} | null)[];
|
|
38
47
|
export default _default;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.looseRestrictSyntaxRules = void 0;
|
|
4
4
|
const globals = require('globals');
|
|
5
5
|
const reactHooks = require('eslint-plugin-react-hooks');
|
|
6
6
|
const tseslint = require('typescript-eslint');
|
|
@@ -32,17 +32,17 @@ const baseSyntaxRules = [
|
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
34
|
// loose模式特化的规则
|
|
35
|
-
exports.
|
|
35
|
+
exports.looseRestrictSyntaxRules = [
|
|
36
36
|
// 约束RoutesComponent路由组件 dom规则
|
|
37
37
|
{
|
|
38
38
|
// 箭头函数 block body
|
|
39
39
|
selector: "VariableDeclarator[id.name='RoutesComponent'] > ArrowFunctionExpression > BlockStatement > ReturnStatement[argument.type='JSXElement'][argument.openingElement.name.name!='Routes']",
|
|
40
|
-
message: 'RoutesComponent must return <Routes> directly without any wrapper',
|
|
40
|
+
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).',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
// 箭头函数 expression body
|
|
44
44
|
selector: "VariableDeclarator[id.name='RoutesComponent'] > ArrowFunctionExpression > JSXElement:not([openingElement.name.name='Routes'])",
|
|
45
|
-
message: 'RoutesComponent must return <Routes> directly without any wrapper',
|
|
45
|
+
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
46
|
},
|
|
47
47
|
];
|
|
48
48
|
// 严格语法规则:仅正常模式启用,loose 模式下不启用
|
|
@@ -94,21 +94,23 @@ const strictSyntaxRules = [
|
|
|
94
94
|
message: 'Classname "text-accent" would cause visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
|
|
95
95
|
},
|
|
96
96
|
];
|
|
97
|
-
const looseSpecificRules = {
|
|
98
|
-
'@lark-apaas/no-nested-styled-jsx': 'error'
|
|
99
|
-
};
|
|
100
97
|
const looseSpecificPlugins = {
|
|
101
98
|
'@lark-apaas': { rules: custom_eslint_rules_1.customRules }
|
|
102
99
|
};
|
|
100
|
+
const looseSpecificRules = {
|
|
101
|
+
'@lark-apaas/no-nested-styled-jsx': 'error'
|
|
102
|
+
};
|
|
103
103
|
// 宽松模式下覆盖的规则(关闭非关键规则)
|
|
104
104
|
const looseOverrideRules = isLooseMode
|
|
105
105
|
? {
|
|
106
|
+
...looseSpecificRules,
|
|
106
107
|
'@typescript-eslint/no-unsafe-function-type': 'off',
|
|
107
108
|
'@typescript-eslint/no-unused-expressions': 'off',
|
|
108
109
|
'no-useless-escape': 'off', // 允许不必要的转义字符
|
|
109
110
|
}
|
|
110
111
|
: {};
|
|
111
|
-
|
|
112
|
+
// 基础配置(适用于所有文件)
|
|
113
|
+
const baseConfig = {
|
|
112
114
|
name: 'fullstack-presets/client-recommend',
|
|
113
115
|
languageOptions: {
|
|
114
116
|
ecmaVersion: 2020,
|
|
@@ -175,9 +177,23 @@ exports.default = {
|
|
|
175
177
|
'no-restricted-syntax': [
|
|
176
178
|
'error',
|
|
177
179
|
...baseSyntaxRules,
|
|
178
|
-
|
|
180
|
+
// 注意:严格模式的 strictSyntaxRules 仍然应用于所有文件
|
|
181
|
+
...(isLooseMode ? [] : strictSyntaxRules),
|
|
179
182
|
],
|
|
180
183
|
// 宽松模式下覆盖上述规则
|
|
181
184
|
...looseOverrideRules,
|
|
182
185
|
},
|
|
183
186
|
};
|
|
187
|
+
// app.tsx 专属配置(仅在 loose 模式下生效)
|
|
188
|
+
const appTsxConfig = isLooseMode
|
|
189
|
+
? {
|
|
190
|
+
// 路由守卫规则
|
|
191
|
+
name: '@lark-apaas/client-recommend/app-tsx',
|
|
192
|
+
files: ['client/src/app.tsx'],
|
|
193
|
+
rules: {
|
|
194
|
+
'no-restricted-syntax': ['error', ...baseSyntaxRules, ...exports.looseRestrictSyntaxRules],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
: null;
|
|
198
|
+
// 导出配置数组,过滤掉 null
|
|
199
|
+
exports.default = [baseConfig, appTsxConfig].filter(Boolean);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const eslintPresets: {
|
|
2
2
|
client: ({
|
|
3
3
|
readonly rules: Readonly<import("eslint").Linter.RulesRecord>;
|
|
4
|
-
} | import("typescript-eslint/dist/compatibility-types").CompatibleConfig | {
|
|
4
|
+
} | import("typescript-eslint/dist/compatibility-types").CompatibleConfig | ({
|
|
5
5
|
name: string;
|
|
6
6
|
languageOptions: {
|
|
7
7
|
ecmaVersion: number;
|
|
@@ -34,6 +34,15 @@ export declare const eslintPresets: {
|
|
|
34
34
|
};
|
|
35
35
|
rules: any;
|
|
36
36
|
} | {
|
|
37
|
+
name: string;
|
|
38
|
+
files: string[];
|
|
39
|
+
rules: {
|
|
40
|
+
'no-restricted-syntax': (string | {
|
|
41
|
+
message: string;
|
|
42
|
+
selector: string;
|
|
43
|
+
})[];
|
|
44
|
+
};
|
|
45
|
+
} | null)[] | {
|
|
37
46
|
ignores: string[];
|
|
38
47
|
})[];
|
|
39
48
|
server: ({
|