@lark-apaas/fullstack-presets 1.1.14 → 1.1.16

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.
@@ -3,4 +3,10 @@ export declare const customRules: {
3
3
  'require-app-container': import("eslint").Rule.RuleModule;
4
4
  'no-direct-capability-api': import("eslint").Rule.RuleModule;
5
5
  'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
6
+ 'require-number-wrapped-count': import("eslint").Rule.RuleModule;
7
+ 'require-shared-response-type': import("eslint").Rule.RuleModule;
8
+ 'require-shared-request-type': import("eslint").Rule.RuleModule;
9
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
10
+ 'require-index-route': import("eslint").Rule.RuleModule;
11
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
6
12
  };
@@ -8,9 +8,21 @@ 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
10
  const require_scroll_reveal_hook_1 = __importDefault(require("./require-scroll-reveal-hook"));
11
+ const require_number_wrapped_count_1 = __importDefault(require("./require-number-wrapped-count"));
12
+ const require_shared_response_type_1 = __importDefault(require("./require-shared-response-type"));
13
+ const require_shared_request_type_1 = __importDefault(require("./require-shared-request-type"));
14
+ const no_welcome_index_route_1 = __importDefault(require("./no-welcome-index-route"));
15
+ const require_index_route_1 = __importDefault(require("./require-index-route"));
16
+ const no_duplicate_route_component_1 = __importDefault(require("./no-duplicate-route-component"));
11
17
  exports.customRules = {
12
18
  'no-nested-styled-jsx': no_nested_styled_jsx_1.default,
13
19
  'require-app-container': require_app_container_1.default,
14
20
  'no-direct-capability-api': no_direct_capability_api_1.default,
15
21
  'require-scroll-reveal-hook': require_scroll_reveal_hook_1.default,
22
+ 'require-number-wrapped-count': require_number_wrapped_count_1.default,
23
+ 'require-shared-response-type': require_shared_response_type_1.default,
24
+ 'require-shared-request-type': require_shared_request_type_1.default,
25
+ 'no-welcome-index-route': no_welcome_index_route_1.default,
26
+ 'require-index-route': require_index_route_1.default,
27
+ 'no-duplicate-route-component': no_duplicate_route_component_1.default,
16
28
  };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint rule to disallow duplicate component usage across Route elements within a <Routes>.
3
+ *
4
+ * This rule enforces that within any <Routes> element, each JSX component used as the
5
+ * `element` prop of a <Route> must be unique. Reusing the same component across
6
+ * multiple routes leads to confusing routing logic.
7
+ *
8
+ * Skips:
9
+ * - Routes without an `element` attribute
10
+ * - element values that are not JSX elements (e.g., variables, null, false)
11
+ */
12
+ import type { Rule } from 'eslint';
13
+ declare const rule: Rule.RuleModule;
14
+ export default rule;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // Serialize a JSXIdentifier or JSXMemberExpression to a stable string key.
4
+ // e.g. JSXIdentifier "Home" → "Home"
5
+ // JSXMemberExpression "Pages.Home" → "Pages.Home"
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ function getJSXName(node) {
8
+ if (!node)
9
+ return null;
10
+ if (node.type === 'JSXIdentifier')
11
+ return node.name;
12
+ if (node.type === 'JSXMemberExpression') {
13
+ const obj = getJSXName(node.object);
14
+ const prop = node.property?.name;
15
+ if (obj && prop)
16
+ return `${obj}.${prop}`;
17
+ }
18
+ return null;
19
+ }
20
+ const rule = {
21
+ meta: {
22
+ type: 'problem',
23
+ docs: {
24
+ description: 'Disallow the same component being used as the element of multiple Routes within a <Routes>',
25
+ category: 'Best Practices',
26
+ recommended: true,
27
+ },
28
+ messages: {
29
+ duplicateRouteComponent: "Component <{{componentName}} /> is already used in another Route's element. Remove the duplicate—keep the index Route if present.",
30
+ },
31
+ schema: [],
32
+ },
33
+ create(context) {
34
+ // Stack of seen-component maps, one entry per nested <Routes>
35
+ const routesStack = [];
36
+ return {
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ JSXElement(node) {
39
+ const tagName = node.openingElement?.name?.name;
40
+ if (tagName === 'Routes') {
41
+ routesStack.push(new Map());
42
+ return;
43
+ }
44
+ if (tagName !== 'Route')
45
+ return;
46
+ if (routesStack.length === 0)
47
+ return;
48
+ const seenComponents = routesStack[routesStack.length - 1];
49
+ const attributes = node.openingElement?.attributes || [];
50
+ // Find the `element` attribute
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ const elementAttr = attributes.find(
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ (attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'element');
55
+ if (!elementAttr)
56
+ return;
57
+ // element value must be a JSXExpressionContainer containing a JSXElement
58
+ const attrValue = elementAttr.value;
59
+ if (attrValue?.type !== 'JSXExpressionContainer')
60
+ return;
61
+ const expression = attrValue.expression;
62
+ if (!expression || expression.type !== 'JSXElement')
63
+ return;
64
+ const componentName = getJSXName(expression.openingElement?.name);
65
+ if (!componentName)
66
+ return;
67
+ if (seenComponents.has(componentName)) {
68
+ context.report({
69
+ node: elementAttr,
70
+ messageId: 'duplicateRouteComponent',
71
+ data: { componentName },
72
+ });
73
+ }
74
+ else {
75
+ seenComponents.set(componentName, elementAttr);
76
+ }
77
+ },
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ 'JSXElement:exit'(node) {
80
+ if (node.openingElement?.name?.name === 'Routes') {
81
+ routesStack.pop();
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
87
+ exports.default = rule;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ESLint rule to disallow Welcome or PagePlaceholder components as the default home page route.
3
+ *
4
+ * This rule enforces that the first-level index route (the actual home page at "/")
5
+ * should use an actual page component rather than placeholder components from the framework.
6
+ *
7
+ * Only checks the first-level index route nested directly under a parent Route (typically Layout).
8
+ * Only flags Welcome/PagePlaceholder if they are imported from @lark-apaas/client-toolkit.
9
+ */
10
+ import type { Rule } from 'eslint';
11
+ declare const rule: Rule.RuleModule;
12
+ export default rule;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rule = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Disallow Welcome or PagePlaceholder from @lark-apaas/client-toolkit as the default home page route element',
8
+ category: 'Best Practices',
9
+ recommended: true,
10
+ },
11
+ messages: {
12
+ noWelcomeIndex: 'Index route should not use {{componentName}} from @lark-apaas/client-toolkit as the element. Please create and use your own home page component (e.g., HomePage, Dashboard).',
13
+ },
14
+ schema: [],
15
+ },
16
+ create(context) {
17
+ // Track imports from @lark-apaas/client-toolkit
18
+ const toolkitImports = new Set();
19
+ return {
20
+ ImportDeclaration(node) {
21
+ const importNode = node;
22
+ const source = importNode.source?.value;
23
+ // Check if importing from @lark-apaas/client-toolkit (any subpath)
24
+ if (typeof source === 'string' &&
25
+ source.startsWith('@lark-apaas/client-toolkit')) {
26
+ // Collect all imported names
27
+ importNode.specifiers?.forEach(specifier => {
28
+ if (specifier.type === 'ImportSpecifier' &&
29
+ specifier.imported?.name) {
30
+ toolkitImports.add(specifier.imported.name);
31
+ }
32
+ });
33
+ }
34
+ },
35
+ JSXElement(node) {
36
+ const element = node;
37
+ const openingElement = element.openingElement;
38
+ if (!openingElement)
39
+ return;
40
+ const elementName = openingElement.name;
41
+ // Check if this is a <Route> element
42
+ if (elementName.type !== 'JSXIdentifier' ||
43
+ elementName.name !== 'Route') {
44
+ return;
45
+ }
46
+ // Check if it has an index prop that is truthy
47
+ const indexAttr = openingElement.attributes.find(attr => attr.type === 'JSXAttribute' &&
48
+ attr.name?.type === 'JSXIdentifier' &&
49
+ attr.name?.name === 'index');
50
+ // If no index attribute, skip this route
51
+ if (!indexAttr) {
52
+ return;
53
+ }
54
+ // Check if index={false} explicitly
55
+ const indexValue = indexAttr.value;
56
+ if (indexValue?.type === 'JSXExpressionContainer' &&
57
+ indexValue.expression?.type === 'Literal' &&
58
+ indexValue.expression.value === false) {
59
+ return;
60
+ }
61
+ // Check if this is a first-level index route (nested under exactly one parent Route)
62
+ // The structure should be: <Routes> -> <Route element={<Layout />}> -> <Route index>
63
+ let routeParentCount = 0;
64
+ let current = element.parent;
65
+ while (current) {
66
+ if (current.type === 'JSXElement') {
67
+ const currentElement = current;
68
+ const currentOpeningElement = currentElement.openingElement;
69
+ const currentElementName = currentOpeningElement?.name;
70
+ if (currentElementName?.type === 'JSXIdentifier' &&
71
+ currentElementName.name === 'Route') {
72
+ routeParentCount++;
73
+ }
74
+ else if (currentElementName?.type === 'JSXIdentifier' &&
75
+ currentElementName.name === 'Routes') {
76
+ // Stop when we reach Routes
77
+ break;
78
+ }
79
+ }
80
+ current = current.parent;
81
+ }
82
+ // Only check if this is a first-level route (exactly 1 parent Route)
83
+ // This corresponds to the default home page pattern:
84
+ // <Routes><Route element={<Layout />}><Route index /></Route></Routes>
85
+ if (routeParentCount !== 1) {
86
+ return;
87
+ }
88
+ // Find the element prop
89
+ const elementProp = openingElement.attributes.find(attr => attr.type === 'JSXAttribute' &&
90
+ attr.name?.type === 'JSXIdentifier' &&
91
+ attr.name?.name === 'element');
92
+ if (!elementProp) {
93
+ return;
94
+ }
95
+ // Check if the element prop value is a JSXExpressionContainer with a JSXElement
96
+ const propValue = elementProp.value;
97
+ if (propValue?.type === 'JSXExpressionContainer' &&
98
+ propValue.expression?.type === 'JSXElement') {
99
+ const jsxElement = propValue.expression;
100
+ const jsxOpeningElement = jsxElement.openingElement;
101
+ const jsxElementName = jsxOpeningElement?.name;
102
+ // Check if the component name is Welcome or PagePlaceholder
103
+ if (jsxElementName?.type === 'JSXIdentifier' &&
104
+ (jsxElementName.name === 'Welcome' ||
105
+ jsxElementName.name === 'PagePlaceholder')) {
106
+ const componentName = jsxElementName.name;
107
+ // Only report if this component was imported from @lark-apaas/client-toolkit
108
+ if (toolkitImports.has(componentName)) {
109
+ context.report({
110
+ node: elementProp,
111
+ messageId: 'noWelcomeIndex',
112
+ data: {
113
+ componentName,
114
+ },
115
+ });
116
+ }
117
+ }
118
+ }
119
+ },
120
+ };
121
+ },
122
+ };
123
+ exports.default = rule;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ESLint rule to require a default index route in RoutesComponent.
3
+ *
4
+ * This rule enforces that the RoutesComponent must have a first-level index route
5
+ * to handle the default "/" path. This ensures users don't see a 404 when visiting
6
+ * the application root.
7
+ *
8
+ * Valid configurations:
9
+ * 1. <Route index element={...} /> (first-level, nested under one parent Route)
10
+ * 2. <Route path="/" element={...} /> (first-level or top-level)
11
+ */
12
+ import type { Rule } from 'eslint';
13
+ declare const rule: Rule.RuleModule;
14
+ export default rule;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rule = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require a default index route in RoutesComponent to handle the root path',
8
+ category: 'Best Practices',
9
+ recommended: true,
10
+ },
11
+ messages: {
12
+ missingIndexRoute: 'RoutesComponent is missing a default home page route. Add either:\n - <Route index element={<HomePage />} /> (nested under Layout)\n - <Route path="/" element={<HomePage />} /> (at any level)',
13
+ },
14
+ schema: [],
15
+ },
16
+ create(context) {
17
+ let inRoutesComponent = false;
18
+ let hasDefaultRoute = false;
19
+ let routeDepth = 0;
20
+ return {
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ VariableDeclarator(node) {
23
+ // Check if this is RoutesComponent
24
+ if (node.id?.name === 'RoutesComponent') {
25
+ inRoutesComponent = true;
26
+ hasDefaultRoute = false;
27
+ routeDepth = 0;
28
+ }
29
+ },
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ 'VariableDeclarator:exit'(node) {
32
+ if (node.id?.name === 'RoutesComponent') {
33
+ if (!hasDefaultRoute) {
34
+ context.report({
35
+ node,
36
+ messageId: 'missingIndexRoute',
37
+ });
38
+ }
39
+ inRoutesComponent = false;
40
+ }
41
+ },
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ JSXElement(node) {
44
+ if (!inRoutesComponent)
45
+ return;
46
+ const elementName = node.openingElement?.name?.name;
47
+ // Track Route depth
48
+ if (elementName === 'Route') {
49
+ routeDepth++;
50
+ // Only check first-level routes (depth 1 or 2)
51
+ // depth 1: <Routes><Route index /></Routes>
52
+ // depth 2: <Routes><Route><Route index /></Route></Routes>
53
+ if (routeDepth <= 2) {
54
+ const attributes = node.openingElement?.attributes || [];
55
+ // Check for index attribute
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ const indexAttr = attributes.find((attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'index');
58
+ if (indexAttr) {
59
+ // Make sure it's not index={false}
60
+ const isIndexFalse = indexAttr.value?.type === 'JSXExpressionContainer' &&
61
+ indexAttr.value.expression?.type === 'Literal' &&
62
+ indexAttr.value.expression.value === false;
63
+ if (!isIndexFalse) {
64
+ hasDefaultRoute = true;
65
+ }
66
+ }
67
+ // Check for path="/"
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const pathAttr = attributes.find((attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'path');
70
+ if (pathAttr) {
71
+ const value = pathAttr.value;
72
+ // Handle path="/"
73
+ if (value?.type === 'Literal' && value.value === '/') {
74
+ hasDefaultRoute = true;
75
+ }
76
+ // Handle path={"/"}
77
+ if (value?.type === 'JSXExpressionContainer' &&
78
+ value.expression?.type === 'Literal' &&
79
+ value.expression.value === '/') {
80
+ hasDefaultRoute = true;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ },
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
+ 'JSXElement:exit'(node) {
88
+ if (!inRoutesComponent)
89
+ return;
90
+ const elementName = node.openingElement?.name?.name;
91
+ if (elementName === 'Route') {
92
+ routeDepth--;
93
+ }
94
+ },
95
+ };
96
+ },
97
+ };
98
+ exports.default = rule;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ESLint 规则:Drizzle count() 返回值必须用 Number(...) 包裹
3
+ *
4
+ * postgres-js 驱动下 Drizzle 的 count() 返回 string 而非 number,
5
+ * 不包裹 Number() 会导致类型不符预期。
6
+ *
7
+ * 通过 import 追踪确认 count 来自 drizzle-orm,避免误匹配其他 count 函数。
8
+ */
9
+ import type { Rule } from 'eslint';
10
+ declare const rule: Rule.RuleModule;
11
+ export default rule;
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const rule = {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require Drizzle count() to be wrapped with Number()',
8
+ category: 'Possible Errors',
9
+ recommended: true,
10
+ },
11
+ schema: [],
12
+ messages: {
13
+ unwrappedCount: 'Drizzle count() returns string in postgres-js. Wrap with Number(count(...)) to get a numeric value.',
14
+ },
15
+ },
16
+ create(context) {
17
+ // 追踪从 drizzle-orm 导入的 count 标识符名
18
+ const drizzleCountNames = new Set();
19
+ return {
20
+ // 收集 import { count } from 'drizzle-orm' 或 import { count as x } from 'drizzle-orm'
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ ImportDeclaration(node) {
23
+ if (node.source.value !== 'drizzle-orm' &&
24
+ !node.source.value.startsWith('drizzle-orm/')) {
25
+ return;
26
+ }
27
+ for (const specifier of node.specifiers) {
28
+ if (specifier.type === 'ImportSpecifier' &&
29
+ specifier.imported.name === 'count') {
30
+ drizzleCountNames.add(specifier.local.name);
31
+ }
32
+ }
33
+ },
34
+ // 检查 count() 调用是否被 Number(...) 包裹
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ CallExpression(node) {
37
+ // 是否是 count(...) 调用
38
+ if (node.callee.type !== 'Identifier' ||
39
+ !drizzleCountNames.has(node.callee.name)) {
40
+ return;
41
+ }
42
+ // 检查父节点是否是 Number(count(...))
43
+ const parent = node.parent;
44
+ if (parent &&
45
+ parent.type === 'CallExpression' &&
46
+ parent.callee.type === 'Identifier' &&
47
+ parent.callee.name === 'Number') {
48
+ return; // 已包裹
49
+ }
50
+ // 检查是否在对象属性值位置且 key 是 count(如 { count: count() })
51
+ // 这种场景下 count() 是 select 的字段定义,不是直接赋值,跳过
52
+ if (parent &&
53
+ parent.type === 'Property' &&
54
+ parent.value === node) {
55
+ // 但如果整个 select result 后来被直接当 number 用,那是使用侧的问题
56
+ // 这里只检查 count() 的直接调用位置
57
+ return;
58
+ }
59
+ context.report({
60
+ node,
61
+ messageId: 'unwrappedCount',
62
+ });
63
+ },
64
+ };
65
+ },
66
+ };
67
+ exports.default = rule;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * ESLint 规则:客户端 HTTP 调用必须使用 shared/ 目录下定义的响应类型
3
+ *
4
+ * 强制前端 API 调用声明响应类型,且类型必须从 shared/ 导入,
5
+ * 让前后端共享类型定义,避免类型不一致。
6
+ *
7
+ * 仅检查调用自有后端接口(URL 以 /api/ 开头)的请求。
8
+ * 三方接口(绝对 URL、其他前缀)跳过,不强制 shared 类型。
9
+ *
10
+ * 检查逻辑(纯 AST):
11
+ * 1. 匹配 axiosForBackend 的 HTTP 调用
12
+ * 2. 检查第一个参数 URL 是否以 /api/ 开头(自有接口)
13
+ * 3. 检查调用是否有泛型参数,且类型从 shared/ 路径导入
14
+ */
15
+ import type { Rule } from 'eslint';
16
+ declare const rule: Rule.RuleModule;
17
+ export default rule;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // 匹配的 HTTP 客户端方法
4
+ const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'request']);
5
+ // 只匹配 axiosForBackend —— 前端统一使用的 HTTP 客户端
6
+ const HTTP_CLIENTS = new Set(['axiosForBackend']);
7
+ const rule = {
8
+ meta: {
9
+ type: 'problem',
10
+ docs: {
11
+ description: 'Require HTTP calls to use response types defined in shared/',
12
+ category: 'Possible Errors',
13
+ recommended: true,
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ missingResponseType: 'HTTP call must declare a response type via generic parameter (e.g. apiClient.get<MyResponse>(...)). Define the type in shared/api.interface.ts.',
18
+ responseTypeNotFromShared: 'HTTP response type "{{typeName}}" must be defined in shared/api.interface.ts. Import it from @shared/ or shared/ path.',
19
+ },
20
+ },
21
+ create(context) {
22
+ // 追踪从 shared/ 路径导入的类型名
23
+ const sharedTypeNames = new Set();
24
+ return {
25
+ // 收集从 shared/ 导入的类型名
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ ImportDeclaration(node) {
28
+ const source = node.source.value;
29
+ if (source.includes('shared/') ||
30
+ source.startsWith('@shared/')) {
31
+ for (const specifier of node.specifiers) {
32
+ if (specifier.local?.name) {
33
+ sharedTypeNames.add(specifier.local.name);
34
+ }
35
+ }
36
+ }
37
+ },
38
+ // 检查 HTTP 调用
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ CallExpression(node) {
41
+ if (!isHttpCall(node))
42
+ return;
43
+ // 只检查自有后端接口(URL 以 /api/ 开头),三方接口跳过
44
+ if (!isOwnApiCall(node))
45
+ return;
46
+ // 检查是否有泛型参数 axiosForBackend.get<Type>(...)
47
+ const typeArgs = node.typeArguments || node.typeParameters;
48
+ if (!typeArgs || typeArgs.params.length === 0) {
49
+ context.report({
50
+ node: node.callee,
51
+ messageId: 'missingResponseType',
52
+ });
53
+ return;
54
+ }
55
+ // 提取第一个泛型参数的类型名
56
+ const typeParam = typeArgs.params[0];
57
+ const typeName = extractTypeName(typeParam);
58
+ if (!typeName)
59
+ return; // 基本类型或无法解析,跳过
60
+ // 检查类型是否从 shared/ 导入
61
+ if (!sharedTypeNames.has(typeName)) {
62
+ context.report({
63
+ node: typeParam,
64
+ messageId: 'responseTypeNotFromShared',
65
+ data: { typeName },
66
+ });
67
+ }
68
+ },
69
+ };
70
+ },
71
+ };
72
+ /**
73
+ * 判断是否是 HTTP 调用:
74
+ * - axios.get/post/...()
75
+ * - apiClient.get/post/...()
76
+ * - fetch() (全局)
77
+ */
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ function isHttpCall(node) {
80
+ const callee = node.callee;
81
+ // axios.get(url) / apiClient.post(url) 等
82
+ // 只匹配已知的 HTTP 客户端对象,避免误匹配业务方法
83
+ // 不匹配 fetch()(原生 fetch 不支持泛型参数)
84
+ if (callee.type === 'MemberExpression' &&
85
+ callee.object.type === 'Identifier' &&
86
+ callee.property.type === 'Identifier' &&
87
+ HTTP_CLIENTS.has(callee.object.name) &&
88
+ HTTP_METHODS.has(callee.property.name)) {
89
+ return true;
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * 判断请求是否是自有后端接口(URL 以 /api/ 开头)。
95
+ * 三方接口(绝对 URL https://...、其他前缀 /open-api/... 等)跳过。
96
+ * 无法解析 URL(变量、模板表达式)时保守跳过,宁漏勿杀。
97
+ */
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ function isOwnApiCall(node) {
100
+ const firstArg = node.arguments?.[0];
101
+ if (!firstArg)
102
+ return false;
103
+ // 字符串字面量: '/api/users'
104
+ if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
105
+ return firstArg.value.startsWith('/api/') || firstArg.value.startsWith('api/');
106
+ }
107
+ // 模板字面量无表达式: `/api/users`
108
+ if (firstArg.type === 'TemplateLiteral' && firstArg.expressions.length === 0) {
109
+ const raw = firstArg.quasis[0]?.value?.raw || '';
110
+ return raw.startsWith('/api/') || raw.startsWith('api/');
111
+ }
112
+ // 模板字面量有表达式: `/api/users/${id}` — 检查开头部分
113
+ if (firstArg.type === 'TemplateLiteral' && firstArg.quasis.length > 0) {
114
+ const raw = firstArg.quasis[0]?.value?.raw || '';
115
+ return raw.startsWith('/api/') || raw.startsWith('api/');
116
+ }
117
+ // 变量或其他动态表达式 — 无法判断,保守跳过
118
+ return false;
119
+ }
120
+ /**
121
+ * 从类型参数 AST 中提取主要类型名。
122
+ */
123
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
124
+ function extractTypeName(typeNode) {
125
+ // SomeType
126
+ if (typeNode.type === 'TSTypeReference' && typeNode.typeName?.type === 'Identifier') {
127
+ return typeNode.typeName.name;
128
+ }
129
+ // SomeType[]
130
+ if (typeNode.type === 'TSArrayType') {
131
+ return extractTypeName(typeNode.elementType);
132
+ }
133
+ // 基本类型 → 跳过
134
+ if (typeNode.type === 'TSVoidKeyword' || typeNode.type === 'TSStringKeyword' ||
135
+ typeNode.type === 'TSNumberKeyword' || typeNode.type === 'TSBooleanKeyword' ||
136
+ typeNode.type === 'TSAnyKeyword') {
137
+ return null;
138
+ }
139
+ return null;
140
+ }
141
+ exports.default = rule;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ESLint 规则:Controller 路由方法的返回类型必须定义在 shared/ 目录下
3
+ *
4
+ * 强制 agent 把 Controller 方法的返回类型写到 shared/api.interface.ts,
5
+ * 让前后端共享类型定义,避免类型不一致。
6
+ *
7
+ * 检查逻辑(纯 AST,不需要 TypeChecker):
8
+ * 1. 找到有 @Controller 装饰器的 class
9
+ * 2. 找到有 @Get/@Post/@Put/@Patch/@Delete 装饰器的方法
10
+ * 3. 检查方法是否有显式返回类型注解
11
+ * 4. 如果有,追踪类型名是否通过 import 从 shared/ 路径导入
12
+ */
13
+ import type { Rule } from 'eslint';
14
+ declare const rule: Rule.RuleModule;
15
+ export default rule;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const HTTP_DECORATORS = new Set(['Get', 'Post', 'Put', 'Patch', 'Delete']);
4
+ const rule = {
5
+ meta: {
6
+ type: 'problem',
7
+ docs: {
8
+ description: 'Require Controller route methods to use return types defined in shared/',
9
+ category: 'Possible Errors',
10
+ recommended: true,
11
+ },
12
+ schema: [],
13
+ messages: {
14
+ missingReturnType: 'Controller route method must have an explicit return type annotation.',
15
+ returnTypeNotFromShared: 'Controller return type "{{typeName}}" must be defined in shared/api.interface.ts. Import it from @shared/ or shared/ path.',
16
+ },
17
+ },
18
+ create(context) {
19
+ // 追踪从 shared/ 路径导入的类型名
20
+ const sharedTypeNames = new Set();
21
+ return {
22
+ // 收集从 shared/ 导入的类型名
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ ImportDeclaration(node) {
25
+ const source = node.source.value;
26
+ if (source.includes('shared/') ||
27
+ source.startsWith('@shared/')) {
28
+ for (const specifier of node.specifiers) {
29
+ if (specifier.local?.name) {
30
+ sharedTypeNames.add(specifier.local.name);
31
+ }
32
+ }
33
+ }
34
+ },
35
+ // 检查 Controller class 的路由方法
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ MethodDefinition(node) {
38
+ // 检查方法是否有 HTTP 装饰器
39
+ const decorators = node.decorators;
40
+ if (!decorators || decorators.length === 0)
41
+ return;
42
+ const hasHttpDecorator = decorators.some((d) => {
43
+ const expr = d.expression;
44
+ // @Get() / @Post() 等
45
+ if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
46
+ return HTTP_DECORATORS.has(expr.callee.name);
47
+ }
48
+ // @Get (无括号,不常见但合法)
49
+ if (expr.type === 'Identifier') {
50
+ return HTTP_DECORATORS.has(expr.name);
51
+ }
52
+ return false;
53
+ });
54
+ if (!hasHttpDecorator)
55
+ return;
56
+ // 检查父节点是否是 @Controller 装饰的 class
57
+ const classNode = node.parent?.parent;
58
+ if (!classNode || classNode.type !== 'ClassDeclaration')
59
+ return;
60
+ const classDecorators = classNode.decorators;
61
+ if (!classDecorators)
62
+ return;
63
+ const hasControllerDecorator = classDecorators.some((d) => {
64
+ const expr = d.expression;
65
+ if (expr.type === 'CallExpression' && expr.callee.type === 'Identifier') {
66
+ return expr.callee.name === 'Controller';
67
+ }
68
+ if (expr.type === 'Identifier') {
69
+ return expr.name === 'Controller';
70
+ }
71
+ return false;
72
+ });
73
+ if (!hasControllerDecorator)
74
+ return;
75
+ // 检查方法是否有显式返回类型
76
+ const returnType = node.value?.returnType?.typeAnnotation;
77
+ if (!returnType) {
78
+ context.report({
79
+ node: node.key,
80
+ messageId: 'missingReturnType',
81
+ });
82
+ return;
83
+ }
84
+ // 提取返回类型名(处理 Promise<T> 的情况)
85
+ const typeName = extractTypeName(returnType);
86
+ if (!typeName)
87
+ return; // 无法解析的复杂类型,跳过
88
+ // 检查类型是否从 shared/ 导入
89
+ if (!sharedTypeNames.has(typeName)) {
90
+ context.report({
91
+ node: node.key,
92
+ messageId: 'returnTypeNotFromShared',
93
+ data: { typeName },
94
+ });
95
+ }
96
+ },
97
+ };
98
+ },
99
+ };
100
+ /**
101
+ * 从返回类型 AST 中提取主要类型名。
102
+ * 处理:Promise<T> → T, T → T, T[] → T
103
+ */
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ function extractTypeName(typeNode) {
106
+ // Promise<SomeType> → 提取 SomeType
107
+ if (typeNode.type === 'TSTypeReference' &&
108
+ typeNode.typeName?.name === 'Promise' &&
109
+ typeNode.typeArguments?.params?.length === 1) {
110
+ return extractTypeName(typeNode.typeArguments.params[0]);
111
+ }
112
+ // SomeType → 直接返回名字
113
+ if (typeNode.type === 'TSTypeReference' && typeNode.typeName?.type === 'Identifier') {
114
+ return typeNode.typeName.name;
115
+ }
116
+ // SomeType[] → 提取 SomeType
117
+ if (typeNode.type === 'TSArrayType') {
118
+ return extractTypeName(typeNode.elementType);
119
+ }
120
+ // 基本类型 (string, number, void, boolean) → 跳过检查
121
+ if (typeNode.type === 'TSVoidKeyword' || typeNode.type === 'TSStringKeyword' ||
122
+ typeNode.type === 'TSNumberKeyword' || typeNode.type === 'TSBooleanKeyword' ||
123
+ typeNode.type === 'TSAnyKeyword') {
124
+ return null;
125
+ }
126
+ return null;
127
+ }
128
+ exports.default = rule;
@@ -19,6 +19,12 @@ declare const _default: {
19
19
  'require-app-container': import("eslint").Rule.RuleModule;
20
20
  'no-direct-capability-api': import("eslint").Rule.RuleModule;
21
21
  'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
22
+ 'require-number-wrapped-count': import("eslint").Rule.RuleModule;
23
+ 'require-shared-response-type': import("eslint").Rule.RuleModule;
24
+ 'require-shared-request-type': import("eslint").Rule.RuleModule;
25
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
26
+ 'require-index-route': import("eslint").Rule.RuleModule;
27
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
22
28
  };
23
29
  };
24
30
  };
@@ -22,6 +22,12 @@ export declare const eslintPresets: {
22
22
  'require-app-container': import("eslint").Rule.RuleModule;
23
23
  'no-direct-capability-api': import("eslint").Rule.RuleModule;
24
24
  'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
25
+ 'require-number-wrapped-count': import("eslint").Rule.RuleModule;
26
+ 'require-shared-response-type': import("eslint").Rule.RuleModule;
27
+ 'require-shared-request-type': import("eslint").Rule.RuleModule;
28
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
29
+ 'require-index-route': import("eslint").Rule.RuleModule;
30
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
25
31
  };
26
32
  };
27
33
  };
@@ -15,14 +15,6 @@ declare const _default: ({
15
15
  globals: any;
16
16
  };
17
17
  plugins: {
18
- '@lark-apaas': {
19
- rules: {
20
- 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
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
- };
25
- };
26
18
  'react-hooks': any;
27
19
  import: any;
28
20
  };
@@ -45,6 +37,9 @@ declare const _default: ({
45
37
  selector: string;
46
38
  message: string;
47
39
  })[];
40
+ '@lark-apaas/no-welcome-index-route': string;
41
+ '@lark-apaas/require-index-route': string;
42
+ '@lark-apaas/no-duplicate-route-component': string;
48
43
  };
49
44
  } | null)[];
50
45
  export default _default;
@@ -7,7 +7,6 @@ const tseslint = require('typescript-eslint');
7
7
  const importPlugin = require('eslint-plugin-import');
8
8
  const fs = require('fs');
9
9
  const path = require('path');
10
- const custom_eslint_rules_1 = require("../../../custom-eslint-rules");
11
10
  // 检查是否启用宽松 lint 模式
12
11
  const isLooseMode = process.env.FORCE_FRAMEWORK_LINT_LOOSE_MODE === 'true';
13
12
  // 读取项目 package.json 中的 flags 配置
@@ -114,10 +113,12 @@ const strictSyntaxRules = [
114
113
  selector: "CallExpression[callee.name='confirm']",
115
114
  },
116
115
  ];
117
- const looseSpecificPlugins = {
118
- '@lark-apaas': { rules: custom_eslint_rules_1.customRules },
119
- };
120
- // 宽松模式下覆盖的规则
116
+ // 宽松模式专用,请勿删除
117
+ // 注意:@lark-apaas 插件在 index.ts 统一注册,这里不再重复
118
+ const looseSpecificPlugins = {};
119
+ // 宽松模式专用,请勿删除
120
+ const looseSpecificRules = {};
121
+ // 宽松模式下覆盖的规则(关闭非关键规则)
121
122
  const looseOverrideRules = isLooseMode
122
123
  ? {
123
124
  '@typescript-eslint/no-unsafe-function-type': 'off',
@@ -168,6 +169,8 @@ const baseConfig = {
168
169
  // 平台规则:禁止直接调用 capability 内部 API,应使用 capabilityClient
169
170
  '@lark-apaas/no-direct-capability-api': 'error',
170
171
  '@lark-apaas/no-nested-styled-jsx': 'error',
172
+ // 自定义规则:HTTP 调用响应类型必须定义在 shared/
173
+ '@lark-apaas/require-shared-request-type': 'error',
171
174
  // TypeScript 规则
172
175
  '@typescript-eslint/no-unused-vars': 'off', // 未使用变量检查关闭
173
176
  '@typescript-eslint/no-explicit-any': 'off', // 允许使用 any 类型
@@ -201,6 +204,12 @@ const baseConfig = {
201
204
  message: "Importing from 'next/link' is prohibited. Please use the `Link` component from 'react-router-dom' for navigation.",
202
205
  },
203
206
  ],
207
+ patterns: [
208
+ {
209
+ group: ['antd/es/table', 'antd/es/table/*', 'antd/lib/table', 'antd/lib/table/*'],
210
+ message: "Importing Table from 'antd' is prohibited. Please use '@lark-apaas/client-toolkit/antd-table' instead.",
211
+ },
212
+ ],
204
213
  },
205
214
  ],
206
215
  'no-restricted-syntax': [
@@ -209,6 +218,8 @@ const baseConfig = {
209
218
  // 注意:严格模式的 strictSyntaxRules 仍然应用于所有文件
210
219
  ...(isLooseMode ? [] : strictSyntaxRules),
211
220
  ],
221
+ // 宽松模式专用规则
222
+ ...looseSpecificRules,
212
223
  // 宽松模式下覆盖上述规则
213
224
  ...looseOverrideRules,
214
225
  },
@@ -225,6 +236,9 @@ const appTsxConfig = isLooseMode
225
236
  ...baseSyntaxRules,
226
237
  ...exports.looseRestrictSyntaxRules,
227
238
  ],
239
+ '@lark-apaas/no-welcome-index-route': 'error',
240
+ '@lark-apaas/require-index-route': 'error',
241
+ '@lark-apaas/no-duplicate-route-component': 'error',
228
242
  },
229
243
  }
230
244
  : null;
@@ -52,6 +52,12 @@ declare const _default: {
52
52
  '@darraghor/nestjs-typed/api-method-should-specify-api-operation': string;
53
53
  '@darraghor/nestjs-typed/api-enum-property-best-practices': string;
54
54
  '@darraghor/nestjs-typed/all-properties-are-whitelisted': string;
55
+ '@lark-apaas/require-number-wrapped-count': string;
56
+ '@lark-apaas/require-shared-response-type': string;
57
+ 'no-restricted-syntax': (string | {
58
+ selector: string;
59
+ message: string;
60
+ })[];
55
61
  };
56
62
  };
57
63
  export default _default;
@@ -73,5 +73,17 @@ exports.default = {
73
73
  '@darraghor/nestjs-typed/api-method-should-specify-api-operation': 'off',
74
74
  '@darraghor/nestjs-typed/api-enum-property-best-practices': 'off',
75
75
  '@darraghor/nestjs-typed/all-properties-are-whitelisted': 'off',
76
+ // 自定义规则:Drizzle count() 必须 Number() 包裹
77
+ '@lark-apaas/require-number-wrapped-count': 'error',
78
+ // 自定义规则:Controller 返回类型必须定义在 shared/
79
+ '@lark-apaas/require-shared-response-type': 'error',
80
+ // Drizzle ORM 规则
81
+ 'no-restricted-syntax': [
82
+ 'error',
83
+ {
84
+ selector: "CallExpression[callee.property.name=/^(values|set)$/] Property[key.name=/^(_created_at|_updated_at|_created_by|_updated_by)$/]",
85
+ message: 'Audit columns (_created_at/_updated_at/_created_by/_updated_by) are auto-populated by the platform. Do not set them in .values() or .set() calls.',
86
+ },
87
+ ],
76
88
  },
77
89
  };
@@ -14,14 +14,6 @@ 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
- '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
- };
24
- };
25
17
  'react-hooks': any;
26
18
  import: any;
27
19
  };
@@ -44,11 +36,50 @@ export declare const eslintPresets: {
44
36
  selector: string;
45
37
  message: string;
46
38
  })[];
39
+ '@lark-apaas/no-welcome-index-route': string;
40
+ '@lark-apaas/require-index-route': string;
41
+ '@lark-apaas/no-duplicate-route-component': string;
47
42
  };
48
43
  } | null)[] | {
44
+ name: string;
45
+ plugins: {
46
+ '@lark-apaas': {
47
+ rules: {
48
+ 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
49
+ 'require-app-container': import("eslint").Rule.RuleModule;
50
+ 'no-direct-capability-api': import("eslint").Rule.RuleModule;
51
+ 'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
52
+ 'require-number-wrapped-count': import("eslint").Rule.RuleModule;
53
+ 'require-shared-response-type': import("eslint").Rule.RuleModule;
54
+ 'require-shared-request-type': import("eslint").Rule.RuleModule;
55
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
56
+ 'require-index-route': import("eslint").Rule.RuleModule;
57
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
58
+ };
59
+ };
60
+ };
61
+ } | {
49
62
  ignores: string[];
50
63
  })[];
51
64
  server: ({
52
65
  readonly rules: Readonly<import("eslint").Linter.RulesRecord>;
53
- } | import("typescript-eslint/dist/compatibility-types").CompatibleConfig)[];
66
+ } | import("typescript-eslint/dist/compatibility-types").CompatibleConfig | {
67
+ name: string;
68
+ plugins: {
69
+ '@lark-apaas': {
70
+ rules: {
71
+ 'no-nested-styled-jsx': import("eslint").Rule.RuleModule;
72
+ 'require-app-container': import("eslint").Rule.RuleModule;
73
+ 'no-direct-capability-api': import("eslint").Rule.RuleModule;
74
+ 'require-scroll-reveal-hook': import("eslint").Rule.RuleModule;
75
+ 'require-number-wrapped-count': import("eslint").Rule.RuleModule;
76
+ 'require-shared-response-type': import("eslint").Rule.RuleModule;
77
+ 'require-shared-request-type': import("eslint").Rule.RuleModule;
78
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
79
+ 'require-index-route': import("eslint").Rule.RuleModule;
80
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
81
+ };
82
+ };
83
+ };
84
+ })[];
54
85
  };
@@ -9,6 +9,7 @@ const eslint_server_1 = __importDefault(require("./eslint-server"));
9
9
  const js_1 = __importDefault(require("@eslint/js"));
10
10
  const typescript_eslint_1 = __importDefault(require("typescript-eslint"));
11
11
  const eslint_plugin_nestjs_typed_1 = __importDefault(require("@darraghor/eslint-plugin-nestjs-typed"));
12
+ const custom_eslint_rules_1 = require("../../../custom-eslint-rules");
12
13
  const testFileIgnorePatterns = [
13
14
  '**/__tests__/**',
14
15
  '**/*.test.js',
@@ -22,6 +23,13 @@ const testFileIgnorePatterns = [
22
23
  ];
23
24
  const globalIgnoreServerPatterns = ['server/database/.introspect/**', ...testFileIgnorePatterns]; // 全局 eslint server ignore
24
25
  const globalIgnoreClientPatterns = ['dist', 'node_modules', 'client/src/api/gen', ...testFileIgnorePatterns]; // 全局 eslint client Ignore
26
+ // @lark-apaas 插件只注册一次,client 和 server config 共享
27
+ const larkApaasPlugin = {
28
+ name: '@lark-apaas/plugin-registration',
29
+ plugins: {
30
+ '@lark-apaas': { rules: custom_eslint_rules_1.customRules },
31
+ },
32
+ };
25
33
  // 只导出纯净的规则配置,不包含基础配置
26
34
  // 用户需要在项目配置中自己添加基础配置(eslintJs, tseslint, nestjs 等)
27
35
  exports.eslintPresets = {
@@ -29,6 +37,7 @@ exports.eslintPresets = {
29
37
  { ignores: globalIgnoreClientPatterns },
30
38
  js_1.default.configs.recommended,
31
39
  ...typescript_eslint_1.default.configs.recommended,
40
+ larkApaasPlugin,
32
41
  eslint_client_1.default
33
42
  ],
34
43
  server: [
@@ -36,6 +45,7 @@ exports.eslintPresets = {
36
45
  js_1.default.configs.recommended,
37
46
  ...typescript_eslint_1.default.configs.recommended,
38
47
  ...eslint_plugin_nestjs_typed_1.default.configs.flatRecommended,
48
+ larkApaasPlugin,
39
49
  eslint_server_1.default
40
50
  ],
41
51
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-presets",
3
- "version": "1.1.14",
3
+ "version": "1.1.16",
4
4
  "files": [
5
5
  "lib"
6
6
  ],