@lark-apaas/fullstack-presets 1.1.2 → 1.1.3-beta.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/README.md CHANGED
@@ -1,2 +1,163 @@
1
1
  # 全栈技术栈 Presets
2
- 独立拆分 simple 目录,属于全栈精简版,目前妙搭场景专用。
2
+ 独立拆分 simple 目录,属于全栈精简版,目前妙搭场景专用。
3
+
4
+ ## ESLint 自定义规则
5
+
6
+ ### `no-nested-styled-jsx`
7
+
8
+ 检测嵌套的 styled-jsx 标签,防止编译错误。https://nextjs.org/docs/messages/nested-styled-jsx-tags
9
+
10
+ 该规则镜像了 styled-jsx babel 插件中的验证逻辑:
11
+ - [babel.js#L215](https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L215)
12
+ - [babel.js#L222](https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L222)
13
+
14
+ #### 使用方式
15
+
16
+ 该规则已在 `eslint-client.ts` 配置中默认启用,无需额外配置:
17
+
18
+ ```typescript
19
+ import clientConfig from '@lark-apaas/fullstack-presets/recommend/eslint/eslint-client';
20
+
21
+ export default [
22
+ clientConfig,
23
+ // 其他配置...
24
+ ];
25
+ ```
26
+
27
+ 如需单独使用该规则:
28
+
29
+ ```typescript
30
+ import { customRules } from '@lark-apaas/fullstack-presets/custom-eslint-rules';
31
+
32
+ export default [
33
+ {
34
+ plugins: {
35
+ '@lark-apaas': { rules: customRules },
36
+ },
37
+ rules: {
38
+ '@lark-apaas/no-nested-styled-jsx': 'error',
39
+ },
40
+ },
41
+ ];
42
+ ```
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
+
54
+ #### 自定义错误信息
55
+
56
+ 可通过配置自定义报错信息:
57
+
58
+ ```typescript
59
+ rules: {
60
+ '@lark-apaas/no-nested-styled-jsx': ['error', {
61
+ message: '禁止嵌套 styled-jsx 标签,请将 <style jsx> 移到组件根级别'
62
+ }],
63
+ }
64
+ ```
65
+
66
+ 注意:使用自定义错误信息时,将不会显示外层 styled-jsx 标签的行号信息。
67
+
68
+ #### 参考资料
69
+
70
+ - [Next.js 错误说明](https://nextjs.org/docs/messages/nested-styled-jsx-tags)
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
+ - 防止开发者误修改导致路由失效
@@ -0,0 +1,3 @@
1
+ export declare const customRules: {
2
+ 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
3
+ };
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.customRules = void 0;
7
+ const no_nested_styled_jsx_1 = __importDefault(require("./no-nested-styled-jsx"));
8
+ exports.customRules = {
9
+ 'no-nested-styled-jsx': no_nested_styled_jsx_1.default,
10
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ESLint rule to detect nested styled-jsx tags.
3
+ *
4
+ * This rule mirrors the validation logic in styled-jsx's babel plugin:
5
+ * @see https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L215
6
+ * @see https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L222
7
+ *
8
+ * Test fixture:
9
+ * @see https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/test/fixtures/nested-style-tags.js
10
+ */
11
+ import type { Rule } from 'eslint';
12
+ declare const rule: Rule.RuleModule;
13
+ export default rule;
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const DEFAULT_MESSAGE = 'Detected nested styled-jsx tag. Read more: https://nextjs.org/docs/messages/nested-styled-jsx-tags';
4
+ const rule = {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Disallow nested styled-jsx tags',
9
+ category: 'Possible Errors',
10
+ recommended: true,
11
+ url: 'https://nextjs.org/docs/messages/nested-styled-jsx-tags',
12
+ },
13
+ schema: [
14
+ {
15
+ type: 'object',
16
+ properties: {
17
+ message: {
18
+ type: 'string',
19
+ description: 'Custom error message',
20
+ },
21
+ },
22
+ additionalProperties: false,
23
+ },
24
+ ],
25
+ messages: {
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',
28
+ },
29
+ },
30
+ create(context) {
31
+ const options = context.options[0];
32
+ const customMessage = options?.message;
33
+ return {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ JSXElement(node) {
36
+ const openingElement = node.openingElement;
37
+ if (!openingElement)
38
+ return;
39
+ const elementName = openingElement.name;
40
+ // Check if this is a <style> element with jsx attribute
41
+ if (elementName.type !== 'JSXIdentifier' || elementName.name !== 'style') {
42
+ return;
43
+ }
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ const hasJsxAttribute = openingElement.attributes.some((attr) => attr.type === 'JSXAttribute' &&
46
+ attr.name?.type === 'JSXIdentifier' &&
47
+ attr.name?.name === 'jsx');
48
+ if (!hasJsxAttribute) {
49
+ return;
50
+ }
51
+ // Count JSXElement and JSXFragment ancestors and find the outer styled-jsx tag
52
+ // styled-jsx uses ignoreClosing counter that increments on JSXOpeningElement enter
53
+ // and checks if ignoreClosing > 1 on style tag exit
54
+ // @see https://github.com/vercel/styled-jsx/blob/d7a59379134d73afaeb98177387cd62d54d746be/src/babel.js#L222
55
+ // Note: JSXFragment (<>...</>) should also be counted as a container
56
+ let jsxElementCount = 0;
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ let outerStyledJsxNode = null;
59
+ let current = node.parent;
60
+ while (current) {
61
+ // Count both JSXElement and JSXFragment as containers
62
+ if (current.type === 'JSXElement' || current.type === 'JSXFragment') {
63
+ jsxElementCount++;
64
+ // Check if any sibling or ancestor siblings contain a styled-jsx tag
65
+ // Always update to find the outermost styled-jsx, not the first encountered
66
+ if (current.parent) {
67
+ const parentNode = current.parent;
68
+ // Check if parent has children that we can iterate
69
+ const children = parentNode.children ||
70
+ (parentNode.type === 'JSXElement' || parentNode.type === 'JSXFragment' ? parentNode.children : null);
71
+ if (children && Array.isArray(children)) {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ for (const child of children) {
74
+ if (child.type === 'JSXElement' && child !== node) {
75
+ const childOpeningElement = child.openingElement;
76
+ if (childOpeningElement) {
77
+ const childElementName = childOpeningElement.name;
78
+ if (childElementName?.type === 'JSXIdentifier' &&
79
+ childElementName?.name === 'style') {
80
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
+ const hasChildJsxAttr = childOpeningElement.attributes?.some((attr) => attr.type === 'JSXAttribute' &&
82
+ attr.name?.type === 'JSXIdentifier' &&
83
+ attr.name?.name === 'jsx');
84
+ if (hasChildJsxAttr) {
85
+ // Always update to find the outermost styled-jsx
86
+ outerStyledJsxNode = child;
87
+ break;
88
+ }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ current = current.parent;
97
+ }
98
+ // If there's more than 1 JSXElement ancestor, the style tag is nested
99
+ if (jsxElementCount > 1) {
100
+ if (outerStyledJsxNode && outerStyledJsxNode.loc) {
101
+ // Report with location information about the outer styled-jsx tag
102
+ context.report({
103
+ node,
104
+ messageId: 'nestedStyleJsxWithLocation',
105
+ data: {
106
+ line: outerStyledJsxNode.loc.start.line,
107
+ },
108
+ });
109
+ }
110
+ else {
111
+ // Fallback to the original message if we can't find the outer styled-jsx tag
112
+ context.report({
113
+ node,
114
+ ...(customMessage ? { message: customMessage } : { messageId: 'nestedStyleJsx' }),
115
+ });
116
+ }
117
+ }
118
+ },
119
+ };
120
+ },
121
+ };
122
+ exports.default = rule;
@@ -1,4 +1,8 @@
1
- declare const _default: {
1
+ export declare const looseRestrictSyntaxRules: {
2
+ selector: string;
3
+ message: string;
4
+ }[];
5
+ declare const _default: ({
2
6
  name: string;
3
7
  languageOptions: {
4
8
  ecmaVersion: number;
@@ -11,6 +15,11 @@ declare const _default: {
11
15
  globals: any;
12
16
  };
13
17
  plugins: {
18
+ '@lark-apaas'?: {
19
+ rules: {
20
+ 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
21
+ };
22
+ } | undefined;
14
23
  'react-hooks': any;
15
24
  import: any;
16
25
  };
@@ -25,5 +34,14 @@ declare const _default: {
25
34
  };
26
35
  };
27
36
  rules: any;
28
- };
37
+ } | {
38
+ name: string;
39
+ files: string[];
40
+ rules: {
41
+ 'no-restricted-syntax': (string | {
42
+ message: string;
43
+ selector: string;
44
+ })[];
45
+ };
46
+ } | null)[];
29
47
  export default _default;
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.looseRestrictSyntaxRules = void 0;
3
4
  const globals = require('globals');
4
5
  const reactHooks = require('eslint-plugin-react-hooks');
5
6
  const tseslint = require('typescript-eslint');
6
7
  const importPlugin = require('eslint-plugin-import');
8
+ const custom_eslint_rules_1 = require("../../../custom-eslint-rules");
7
9
  // 检查是否启用宽松 lint 模式
8
10
  const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
9
11
  // 基础语法规则:所有模式都启用(包括 loose 模式)
@@ -29,6 +31,20 @@ const baseSyntaxRules = [
29
31
  message: "Please don't use relative paths in <a> tags. Use NavLink from 'react-router-dom' instead.",
30
32
  },
31
33
  ];
34
+ // loose模式特化的规则
35
+ exports.looseRestrictSyntaxRules = [
36
+ // 约束RoutesComponent路由组件 dom规则
37
+ {
38
+ // 箭头函数 block body
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. Do not add <div>, <BrowserRouter> or other wrappers. You can only modify routes inside <Routes> (add/edit/remove <Route> elements).',
41
+ },
42
+ {
43
+ // 箭头函数 expression body
44
+ selector: "VariableDeclarator[id.name='RoutesComponent'] > ArrowFunctionExpression > JSXElement:not([openingElement.name.name='Routes'])",
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
+ },
47
+ ];
32
48
  // 严格语法规则:仅正常模式启用,loose 模式下不启用
33
49
  const strictSyntaxRules = [
34
50
  // 限制使用 fetch(推荐使用生成的 API Client)
@@ -78,15 +94,23 @@ const strictSyntaxRules = [
78
94
  message: 'Classname "text-accent" would cause visibility issues. Consider using proper semantic color tokens from `client/src/tailwind-theme.css`',
79
95
  },
80
96
  ];
97
+ const looseSpecificPlugins = {
98
+ '@lark-apaas': { rules: custom_eslint_rules_1.customRules }
99
+ };
100
+ const looseSpecificRules = {
101
+ '@lark-apaas/no-nested-styled-jsx': 'error'
102
+ };
81
103
  // 宽松模式下覆盖的规则(关闭非关键规则)
82
104
  const looseOverrideRules = isLooseMode
83
105
  ? {
106
+ ...looseSpecificRules,
84
107
  '@typescript-eslint/no-unsafe-function-type': 'off',
85
108
  '@typescript-eslint/no-unused-expressions': 'off',
86
109
  'no-useless-escape': 'off', // 允许不必要的转义字符
87
110
  }
88
111
  : {};
89
- exports.default = {
112
+ // 基础配置(适用于所有文件)
113
+ const baseConfig = {
90
114
  name: 'fullstack-presets/client-recommend',
91
115
  languageOptions: {
92
116
  ecmaVersion: 2020,
@@ -104,6 +128,7 @@ exports.default = {
104
128
  plugins: {
105
129
  'react-hooks': reactHooks,
106
130
  import: importPlugin,
131
+ ...(isLooseMode ? looseSpecificPlugins : {}),
107
132
  },
108
133
  settings: {
109
134
  'import/resolver': {
@@ -149,8 +174,26 @@ exports.default = {
149
174
  ],
150
175
  },
151
176
  ],
152
- 'no-restricted-syntax': ['error', ...baseSyntaxRules, ...(isLooseMode ? [] : strictSyntaxRules)],
177
+ 'no-restricted-syntax': [
178
+ 'error',
179
+ ...baseSyntaxRules,
180
+ // 注意:严格模式的 strictSyntaxRules 仍然应用于所有文件
181
+ ...(isLooseMode ? [] : strictSyntaxRules),
182
+ ],
153
183
  // 宽松模式下覆盖上述规则
154
184
  ...looseOverrideRules,
155
185
  },
156
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;
@@ -14,6 +14,11 @@ export declare const eslintPresets: {
14
14
  globals: any;
15
15
  };
16
16
  plugins: {
17
+ '@lark-apaas'?: {
18
+ rules: {
19
+ 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
20
+ };
21
+ } | undefined;
17
22
  'react-hooks': any;
18
23
  import: any;
19
24
  };
@@ -29,6 +34,15 @@ export declare const eslintPresets: {
29
34
  };
30
35
  rules: any;
31
36
  } | {
37
+ name: string;
38
+ files: string[];
39
+ rules: {
40
+ 'no-restricted-syntax': (string | {
41
+ message: string;
42
+ selector: string;
43
+ })[];
44
+ };
45
+ } | null)[] | {
32
46
  ignores: string[];
33
47
  })[];
34
48
  server: ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-presets",
3
- "version": "1.1.2",
3
+ "version": "1.1.3-beta.0",
4
4
  "files": [
5
5
  "lib"
6
6
  ],
@@ -18,6 +18,7 @@
18
18
  "build": "tsc && npm run copy:json",
19
19
  "copy:json": "node scripts/copy-json.js",
20
20
  "watch": "tsc --watch",
21
+ "test": "vitest",
21
22
  "prepublishOnly": "npm run build"
22
23
  },
23
24
  "dependencies": {
@@ -33,8 +34,11 @@
33
34
  "tailwindcss-animate": "^1.0.7"
34
35
  },
35
36
  "devDependencies": {
37
+ "@babel/core": "^7.24.0",
38
+ "@babel/preset-react": "^7.24.0",
36
39
  "@types/eslint": "^9.6.0",
37
40
  "eslint": "^9.35.0",
41
+ "styled-jsx": "^5.1.6",
38
42
  "typescript": "^5.9.2",
39
43
  "typescript-eslint": "^8.44.0"
40
44
  },