@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
- context.report({
65
- node,
66
- ...(customMessage ? { message: customMessage } : { messageId: 'nestedStyleJsx' }),
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 looseSpecificSyntaxRules: {
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.looseSpecificSyntaxRules = void 0;
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.looseSpecificSyntaxRules = [
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
- exports.default = {
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
- ...(isLooseMode ? exports.looseSpecificSyntaxRules : strictSyntaxRules),
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: ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-presets",
3
- "version": "1.1.3-beta.1",
3
+ "version": "1.1.3-beta.3",
4
4
  "files": [
5
5
  "lib"
6
6
  ],