@lark-apaas/coding-presets-react 0.1.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/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @lark-apaas/coding-presets-react
2
+
3
+ Shared ESLint, Stylelint, Tailwind, and TypeScript presets for OpenClaw coding templates.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @lark-apaas/coding-presets-react
9
+ ```
10
+
11
+ ## ESLint
12
+
13
+ Provides a client preset with `eslint/recommended` + `typescript-eslint/recommended` base configs.
14
+
15
+ ```js
16
+ // eslint.config.js
17
+ import { eslintPresets } from '@lark-apaas/coding-presets-react'
18
+
19
+ export default [
20
+ // Client: React + TypeScript
21
+ ...eslintPresets.client.map(config => config.ignores ? config : {
22
+ ...config,
23
+ files: ['src/**/*.{ts,tsx}'],
24
+ }),
25
+ ]
26
+ ```
27
+
28
+ ### Client rules include
29
+
30
+ - React Hooks recommended rules (exhaustive-deps off)
31
+ - TypeScript loose mode (no-unused-vars, no-explicit-any, no-empty-object-type off)
32
+ - Restricted syntax: `location.href` assignment, relative `<a>` href, Tailwind 4 hsl/rgb space check
33
+ - Restricted imports: `next/link` prohibited
34
+
35
+ ## TypeScript
36
+
37
+ Provides `tsconfig.app.json` and `tsconfig.node.json` presets aligned with Vite 8 standards.
38
+
39
+ ```jsonc
40
+ // tsconfig.app.json — Client (React)
41
+ {
42
+ "extends": "@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json",
43
+ "compilerOptions": {
44
+ "types": ["vite/client"],
45
+ "paths": { "@/*": ["./client/src/*"] }
46
+ },
47
+ "include": ["client/src"]
48
+ }
49
+ ```
50
+
51
+ ```jsonc
52
+ // tsconfig.node.json — Tooling (vite.config.ts, etc.)
53
+ {
54
+ "extends": "@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.node.json",
55
+ "include": ["vite.config.ts"]
56
+ }
57
+ ```
58
+
59
+ ### TSConfig presets settings
60
+
61
+ | Option | app | node |
62
+ |--------|-----|------|
63
+ | target | ES2023 | ES2023 |
64
+ | module | ESNext | ESNext |
65
+ | moduleResolution | bundler | bundler |
66
+ | strict | true | true |
67
+ | jsx | react-jsx | — |
68
+ | verbatimModuleSyntax | true | true |
69
+ | noEmit | true | true |
70
+
71
+ ## Stylelint
72
+
73
+ ```js
74
+ import { stylelintPresets } from '@lark-apaas/coding-presets-react'
75
+ // Use stylelintPresets as your stylelint config
76
+ ```
77
+
78
+ Includes custom HSL validation rule and duplicate custom properties check.
79
+
80
+ ## Tailwind
81
+
82
+ ```js
83
+ import { createTailwindPreset } from '@lark-apaas/coding-presets-react'
84
+ const preset = createTailwindPreset({ content: ['./src/**/*.tsx'] })
85
+ ```
86
+
87
+ Includes `tailwindcss-animate` plugin with class-based dark mode.
88
+
89
+ ## License
90
+
91
+ MIT
@@ -0,0 +1,5 @@
1
+ export declare const customRules: {
2
+ 'no-welcome-index-route': import("eslint").Rule.RuleModule;
3
+ 'require-index-route': import("eslint").Rule.RuleModule;
4
+ 'no-duplicate-route-component': import("eslint").Rule.RuleModule;
5
+ };
@@ -0,0 +1,14 @@
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_welcome_index_route_1 = __importDefault(require("./no-welcome-index-route"));
8
+ const require_index_route_1 = __importDefault(require("./require-index-route"));
9
+ const no_duplicate_route_component_1 = __importDefault(require("./no-duplicate-route-component"));
10
+ exports.customRules = {
11
+ 'no-welcome-index-route': no_welcome_index_route_1.default,
12
+ 'require-index-route': require_index_route_1.default,
13
+ 'no-duplicate-route-component': no_duplicate_route_component_1.default,
14
+ };
@@ -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-lite.
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-lite 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-lite 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-lite
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-lite (any subpath)
24
+ if (typeof source === 'string' &&
25
+ source.startsWith('@lark-apaas/client-toolkit-lite')) {
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-lite
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,7 @@
1
+ export declare const ruleName = "custom/hsl-valid-value";
2
+ export declare const messages: any;
3
+ export declare const meta: {
4
+ url: string;
5
+ };
6
+ declare const _default: any;
7
+ export default _default;
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.meta = exports.messages = exports.ruleName = void 0;
40
+ /* eslint-disable */
41
+ // @ts-nocheck
42
+ /**
43
+ * 自定义 Stylelint 规则:校验 CSS 声明中非法的 hsl() 写法
44
+ *
45
+ * 遍历所有 CSS 声明(包括自定义属性 --*),当值以 hsl()/hsla() 开头时,
46
+ * 使用 css-tree 的词法器验证其是否符合 CSS color 规范。
47
+ *
48
+ * 注意:只处理纯颜色值声明,不处理复合值(如 box-shadow、background 等)中的 hsl()。
49
+ *
50
+ * 非法示例:
51
+ * --color: hsl(100%, 50%, 26%); // hue 不能是百分比
52
+ * --color: hsl(200 50% 26% 0.5); // alpha 缺少斜杠
53
+ * --color: hsl(200 50%); // 缺少 lightness
54
+ * color: hsl(); // 空参数
55
+ *
56
+ * 合法示例:
57
+ * --color: hsl(200, 50%, 26%); // CSS3 逗号写法
58
+ * --color: hsl(200 50% 26%); // CSS4 空格写法
59
+ * --color: hsl(200deg 50% 26%); // 带单位的 hue
60
+ * --color: hsl(200 50% 26% / 0.5); // CSS4 带 alpha
61
+ *
62
+ * 跳过(不检查)的示例:
63
+ * --shadow: 0px 2px 12px hsl(0 0% 0% / 0.01); // 复合值中的 hsl
64
+ * background: linear-gradient(hsl(...), ...); // 复合值中的 hsl
65
+ * --border: hsl(from hsl(217 80% 55%) h s calc(l + var(--x))); // CSS Color Level 5 相对颜色语法
66
+ * --color: hsl(none 50% 26%); // none 关键字(CSS Color Level 4)
67
+ *
68
+ * var() 通道的处理:
69
+ * 对每个 var() 独立尝试占位符 0 和 0%(hue 位置需要数字,sat/light 位置需要百分比),
70
+ * 穷举所有 2^n 组合,只要存在一种合法结构就认为该写法合法。
71
+ * 这样可以在不误报合法写法的前提下,仍然捕获结构性错误(如缺少参数)。
72
+ */
73
+ const stylelint_1 = __importDefault(require("stylelint"));
74
+ const csstree = __importStar(require("css-tree"));
75
+ exports.ruleName = 'custom/hsl-valid-value';
76
+ const { report, ruleMessages, validateOptions } = stylelint_1.default.utils;
77
+ exports.messages = ruleMessages(exports.ruleName, {
78
+ invalidHsl: (value) => `Invalid hsl() value: "${value}". Refer to https://developer.mozilla.org/docs/Web/CSS/color_value/hsl`,
79
+ });
80
+ exports.meta = {
81
+ url: 'https://github.com/csstree/stylelint-validator',
82
+ };
83
+ /**
84
+ * 提取字符串中所有顶层 var() 的位置和长度,使用括号计数支持任意深度嵌套。
85
+ * 例如 var(--a, var(--b, var(--c, 0))) 会被作为一个整体提取。
86
+ */
87
+ function extractVarRanges(value) {
88
+ const ranges = [];
89
+ const varStart = /var\s*\(/gi;
90
+ let m;
91
+ while ((m = varStart.exec(value)) !== null) {
92
+ let depth = 1;
93
+ let j = m.index + m[0].length;
94
+ while (j < value.length && depth > 0) {
95
+ if (value[j] === '(')
96
+ depth++;
97
+ else if (value[j] === ')')
98
+ depth--;
99
+ j++;
100
+ }
101
+ ranges.push({ index: m.index, length: j - m.index });
102
+ varStart.lastIndex = j; // 跳过已处理的 var(),避免重复匹配内层
103
+ }
104
+ return ranges;
105
+ }
106
+ /**
107
+ * 对 hsl() 值中的每个 var() 分别尝试占位符 "0" 和 "0%",
108
+ * 穷举 2^n 种组合,只要任意一种通过 css-tree 词法验证就返回 true。
109
+ *
110
+ * 背景:css-tree 不能解析 var(),但不同通道对占位符类型要求不同:
111
+ * - hue 位置需要数字(0),saturation/lightness 位置需要百分比(0%)。
112
+ * 穷举组合避免了需要感知通道位置的复杂解析。
113
+ */
114
+ function isHslValidWithVarPlaceholders(value, ranges) {
115
+ const isValidCssColor = (v) => {
116
+ try {
117
+ const ast = csstree.parse(v, { context: 'value' });
118
+ return !csstree.lexer.matchType('color', ast).error;
119
+ }
120
+ catch {
121
+ return false;
122
+ }
123
+ };
124
+ const n = ranges.length;
125
+ const total = 1 << n; // 2^n 种组合
126
+ for (let mask = 0; mask < total; mask++) {
127
+ let replaced = value;
128
+ let offset = 0;
129
+ for (let i = 0; i < n; i++) {
130
+ const placeholder = (mask >> i) & 1 ? '0%' : '0';
131
+ const m = ranges[i];
132
+ const start = m.index + offset;
133
+ const end = start + m.length;
134
+ replaced = replaced.slice(0, start) + placeholder + replaced.slice(end);
135
+ offset += placeholder.length - m.length;
136
+ }
137
+ if (isValidCssColor(replaced))
138
+ return true;
139
+ }
140
+ return false; // 所有组合均非法,视为结构性错误
141
+ }
142
+ const rule = primary => {
143
+ return (root, result) => {
144
+ const validOptions = validateOptions(result, exports.ruleName, {
145
+ actual: primary,
146
+ possible: [true],
147
+ });
148
+ if (!validOptions || !primary)
149
+ return;
150
+ root.walkDecls(decl => {
151
+ const value = decl.value.trim();
152
+ // 只处理纯 hsl/hsla 颜色值,跳过复合值(如 box-shadow)
153
+ if (!/^hsla?\(/i.test(value))
154
+ return;
155
+ // 跳过 CSS Color Level 5 相对颜色语法:hsl(from <color> h s l)
156
+ // css-tree 词法器尚不支持该语法,直接跳过避免误报
157
+ if (/^hsla?\(\s*from\s+/i.test(value))
158
+ return;
159
+ // none 关键字只在现代空格语法中合法(CSS Color Level 4),逗号语法不支持
160
+ // 通过是否含逗号区分语法:现代语法跳过,逗号语法交由 css-tree 验证
161
+ const isLegacySyntax = /^hsla?\s*\([^)]*,/i.test(value);
162
+ if (!isLegacySyntax && /\bnone\b/i.test(value))
163
+ return;
164
+ // 包含 var() 时,穷举占位符组合进行结构性校验
165
+ const varRanges = extractVarRanges(value);
166
+ if (varRanges.length > 0) {
167
+ if (!isHslValidWithVarPlaceholders(value, varRanges)) {
168
+ report({
169
+ message: exports.messages.invalidHsl(value),
170
+ node: decl,
171
+ result,
172
+ ruleName: exports.ruleName,
173
+ word: value,
174
+ });
175
+ }
176
+ return;
177
+ }
178
+ try {
179
+ const ast = csstree.parse(value, { context: 'value' });
180
+ const matchResult = csstree.lexer.matchType('color', ast);
181
+ if (matchResult.error) {
182
+ report({
183
+ message: exports.messages.invalidHsl(value),
184
+ node: decl,
185
+ result,
186
+ ruleName: exports.ruleName,
187
+ word: value,
188
+ });
189
+ }
190
+ }
191
+ catch {
192
+ // css-tree parse 失败也视为非法值
193
+ report({
194
+ message: exports.messages.invalidHsl(value),
195
+ node: decl,
196
+ result,
197
+ ruleName: exports.ruleName,
198
+ word: value,
199
+ });
200
+ }
201
+ });
202
+ };
203
+ };
204
+ rule.ruleName = exports.ruleName;
205
+ rule.messages = exports.messages;
206
+ rule.meta = exports.meta;
207
+ exports.default = stylelint_1.default.createPlugin(exports.ruleName, rule);
@@ -0,0 +1,3 @@
1
+ import type { Linter } from 'eslint';
2
+ declare const configs: Linter.Config[];
3
+ export default configs;
@@ -0,0 +1,119 @@
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
+ /* eslint-disable */
7
+ const globals_1 = __importDefault(require("globals"));
8
+ const custom_eslint_rules_1 = require("../custom-eslint-rules");
9
+ const tsEslint = require('typescript-eslint');
10
+ const reactHooks = require('eslint-plugin-react-hooks');
11
+ const importPlugin = require('eslint-plugin-import');
12
+ // 基础语法限制规则
13
+ const restrictedSyntaxRules = [
14
+ // 禁止 window.location.href 赋值
15
+ {
16
+ selector: 'AssignmentExpression[left.object.object.name="window"][left.object.property.name="location"][left.property.name="href"]',
17
+ message: "Don't use `window.location.href` to navigate. Use `useNavigate` from 'react-router-dom' instead.",
18
+ },
19
+ // 禁止 location.href 赋值
20
+ {
21
+ selector: 'AssignmentExpression[left.object.name="location"][left.property.name="href"]',
22
+ message: "Don't use `location.href` to navigate. Use `useNavigate` from 'react-router-dom' instead.",
23
+ },
24
+ // 禁止 a 标签 href 使用相对路径
25
+ {
26
+ selector: "JSXOpeningElement[name.name='a']:has(JSXAttribute[name.name='href'][value.value=/^(?!https?:|\\u002F\\u002F|mailto:|tel:|#).+/])",
27
+ message: "Don't use relative paths in <a> tags. Use NavLink from 'react-router-dom' instead.",
28
+ },
29
+ // Tailwind 4 arbitrary values 不能包含空格的 hsl/rgb
30
+ {
31
+ selector: 'JSXAttribute[name.name="className"][value.value=/\\[hsl\\([^\\]]*\\s[^\\]]*\\)/]',
32
+ message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in hsl() values.',
33
+ },
34
+ {
35
+ selector: 'JSXAttribute[name.name="className"][value.value=/\\[rgb\\([^\\]]*\\s[^\\]]*\\)/]',
36
+ message: 'Tailwind 4 arbitrary values cannot contain spaces. Replace spaces with underscores in rgb() values.',
37
+ },
38
+ ];
39
+ /** 客户端 ESLint 配置 */
40
+ const clientConfig = {
41
+ name: 'coding-presets/client',
42
+ languageOptions: {
43
+ ecmaVersion: 2020,
44
+ parser: tsEslint.parser,
45
+ parserOptions: {
46
+ ecmaFeatures: { jsx: true },
47
+ },
48
+ globals: {
49
+ ...globals_1.default.browser,
50
+ ...globals_1.default.node,
51
+ },
52
+ },
53
+ plugins: {
54
+ 'react-hooks': reactHooks,
55
+ import: importPlugin,
56
+ '@lark-apaas': { rules: custom_eslint_rules_1.customRules },
57
+ },
58
+ settings: {
59
+ 'import/resolver': {
60
+ node: { extensions: ['.js', '.jsx', '.ts', '.tsx'] },
61
+ },
62
+ 'import/parsers': {
63
+ '@typescript-eslint/parser': ['.ts', '.tsx'],
64
+ },
65
+ },
66
+ rules: {
67
+ // React Hooks
68
+ ...reactHooks.configs.recommended.rules,
69
+ 'react-hooks/exhaustive-deps': 'off',
70
+ // TypeScript(宽松模式)
71
+ '@typescript-eslint/no-unused-vars': 'off',
72
+ '@typescript-eslint/no-explicit-any': 'off',
73
+ '@typescript-eslint/no-empty-interface': 'off',
74
+ '@typescript-eslint/no-empty-object-type': 'off',
75
+ '@typescript-eslint/no-redeclare': 'error',
76
+ '@typescript-eslint/no-unsafe-function-type': 'off',
77
+ '@typescript-eslint/no-unused-expressions': 'off',
78
+ '@typescript-eslint/no-require-imports': 'off',
79
+ // JavaScript 基础
80
+ 'no-undef': 'off',
81
+ 'no-empty': 'off',
82
+ 'no-unused-labels': 'off',
83
+ 'no-console': 'off',
84
+ 'prefer-const': 'off',
85
+ 'no-control-regex': 'off',
86
+ 'no-useless-escape': 'off',
87
+ 'no-case-declarations': 'off',
88
+ 'no-constant-binary-expression': 'off',
89
+ // 禁止导入 next/link
90
+ 'no-restricted-imports': [
91
+ 'error',
92
+ {
93
+ paths: [
94
+ {
95
+ name: 'next/link',
96
+ message: "Importing from 'next/link' is prohibited. Use `Link` from 'react-router-dom' instead.",
97
+ },
98
+ ],
99
+ },
100
+ ],
101
+ // 语法限制
102
+ 'no-restricted-syntax': ['error', ...restrictedSyntaxRules],
103
+ },
104
+ };
105
+ /** app.tsx 专用配置:启用路由相关 lint 规则
106
+ * files 同时匹配老全栈模板(client/src/app.tsx)和新 jsPage 扁平模板(src/app.tsx)
107
+ */
108
+ const appTsxConfig = {
109
+ name: 'coding-presets/client/app-tsx',
110
+ files: ['client/src/app.tsx', 'src/app.tsx'],
111
+ rules: {
112
+ '@lark-apaas/no-welcome-index-route': 'error',
113
+ '@lark-apaas/require-index-route': 'error',
114
+ '@lark-apaas/no-duplicate-route-component': 'error',
115
+ },
116
+ };
117
+ // 显式注解 portable type,避免 declaration emit 时引用到 .pnpm hoisted 路径(TS2742)
118
+ const configs = [clientConfig, appTsxConfig];
119
+ exports.default = configs;
@@ -0,0 +1,6 @@
1
+ import type { Linter } from 'eslint';
2
+ /** ESLint 预设 */
3
+ export declare const eslintPresets: {
4
+ /** 客户端 ESLint 配置(含 eslint/recommended + typescript-eslint/recommended) */
5
+ client: Linter.Config[];
6
+ };
@@ -0,0 +1,29 @@
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.eslintPresets = void 0;
7
+ const js_1 = __importDefault(require("@eslint/js"));
8
+ const typescript_eslint_1 = __importDefault(require("typescript-eslint"));
9
+ const eslint_client_1 = __importDefault(require("./eslint-client"));
10
+ const testFileIgnorePatterns = [
11
+ '**/__tests__/**',
12
+ '**/*.test.{js,jsx,ts,tsx}',
13
+ '**/*.spec.{js,jsx,ts,tsx}',
14
+ ];
15
+ const globalIgnorePatterns = [
16
+ 'dist',
17
+ 'node_modules',
18
+ ...testFileIgnorePatterns,
19
+ ];
20
+ /** ESLint 预设 */
21
+ exports.eslintPresets = {
22
+ /** 客户端 ESLint 配置(含 eslint/recommended + typescript-eslint/recommended) */
23
+ client: [
24
+ { ignores: globalIgnorePatterns },
25
+ js_1.default.configs.recommended,
26
+ ...typescript_eslint_1.default.configs.recommended,
27
+ ...eslint_client_1.default,
28
+ ],
29
+ };
package/lib/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export { eslintPresets } from './eslint';
2
+ export { stylelintPresets } from './stylelint';
3
+ export { createTailwindPreset } from './tailwind';
4
+ export type { TailwindPresetOptions } from './tailwind';
5
+ export declare const tsconfigPresets: {
6
+ /** 客户端 tsconfig(Vite + React) */
7
+ readonly app: "@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json";
8
+ /** Node 端 tsconfig(vite.config 等工具链) */
9
+ readonly node: "@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.node.json";
10
+ };
package/lib/index.js ADDED
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tsconfigPresets = exports.createTailwindPreset = exports.stylelintPresets = exports.eslintPresets = void 0;
4
+ // ESLint 配置
5
+ var eslint_1 = require("./eslint");
6
+ Object.defineProperty(exports, "eslintPresets", { enumerable: true, get: function () { return eslint_1.eslintPresets; } });
7
+ // Stylelint 配置
8
+ var stylelint_1 = require("./stylelint");
9
+ Object.defineProperty(exports, "stylelintPresets", { enumerable: true, get: function () { return stylelint_1.stylelintPresets; } });
10
+ // Tailwind 配置
11
+ var tailwind_1 = require("./tailwind");
12
+ Object.defineProperty(exports, "createTailwindPreset", { enumerable: true, get: function () { return tailwind_1.createTailwindPreset; } });
13
+ // TSConfig 预设路径(用于 tsconfig.json 的 extends 字段)
14
+ // 示例: { "extends": "@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json" }
15
+ exports.tsconfigPresets = {
16
+ /** 客户端 tsconfig(Vite + React) */
17
+ app: '@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.app.json',
18
+ /** Node 端 tsconfig(vite.config 等工具链) */
19
+ node: '@lark-apaas/coding-presets-react/lib/tsconfig/tsconfig.node.json',
20
+ };
@@ -0,0 +1,8 @@
1
+ /** OpenClaw Stylelint 预设 */
2
+ export declare const stylelintPresets: {
3
+ plugins: any[];
4
+ rules: {
5
+ 'declaration-block-no-duplicate-custom-properties': boolean;
6
+ "custom/hsl-valid-value": boolean;
7
+ };
8
+ };
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.stylelintPresets = void 0;
37
+ const hsl_variable_1 = __importStar(require("../custom-stylelint-rules/hsl-variable"));
38
+ /** OpenClaw Stylelint 预设 */
39
+ exports.stylelintPresets = {
40
+ plugins: [hsl_variable_1.default],
41
+ rules: {
42
+ 'declaration-block-no-duplicate-custom-properties': true,
43
+ [hsl_variable_1.ruleName]: true,
44
+ },
45
+ };
@@ -0,0 +1,12 @@
1
+ export interface TailwindPresetOptions {
2
+ /** 额外的 content 扫描路径 */
3
+ content?: string[];
4
+ }
5
+ /** 创建 OpenClaw Tailwind 预设 */
6
+ export declare function createTailwindPreset(options?: TailwindPresetOptions): {
7
+ darkMode: "class";
8
+ content: string[];
9
+ plugins: {
10
+ handler: () => void;
11
+ }[];
12
+ };
@@ -0,0 +1,15 @@
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.createTailwindPreset = createTailwindPreset;
7
+ const tailwindcss_animate_1 = __importDefault(require("tailwindcss-animate"));
8
+ /** 创建 OpenClaw Tailwind 预设 */
9
+ function createTailwindPreset(options = {}) {
10
+ return {
11
+ darkMode: 'class',
12
+ content: options.content ?? [],
13
+ plugins: [tailwindcss_animate_1.default],
14
+ };
15
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": false,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": false,
19
+ "noUnusedLocals": false,
20
+ "noUnusedParameters": false,
21
+ "noImplicitAny": false,
22
+ "noFallthroughCasesInSwitch": false
23
+ }
24
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+
8
+ /* Bundler mode */
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "verbatimModuleSyntax": false,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+
15
+ /* Linting */
16
+ "strict": false,
17
+ "noUnusedLocals": false,
18
+ "noUnusedParameters": false,
19
+
20
+ "types": ["node"]
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@lark-apaas/coding-presets-react",
3
+ "version": "0.1.0",
4
+ "description": "ESLint, Stylelint, and Tailwind presets for React front-end projects",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
+ "files": [
8
+ "lib"
9
+ ],
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "openclaw",
13
+ "presets",
14
+ "eslint",
15
+ "stylelint",
16
+ "tailwind"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc && node scripts/copy-json.js",
23
+ "watch": "tsc --watch",
24
+ "test": "vitest run",
25
+ "test:watch": "vitest",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "dependencies": {
29
+ "@eslint/js": "^9.35.0",
30
+ "css-tree": "^3.1.0",
31
+ "eslint-plugin-import": "^2.32.0",
32
+ "eslint-plugin-react-hooks": "^5.2.0",
33
+ "globals": "^16.4.0",
34
+ "stylelint": "^17.3.0",
35
+ "tailwindcss-animate": "^1.0.7",
36
+ "typescript-eslint": "^8.44.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/eslint": "^9.6.0",
40
+ "@types/node": "^22.0.0",
41
+ "eslint": "^9.35.0",
42
+ "typescript": "^5.9.2",
43
+ "vitest": "^3.2.4"
44
+ },
45
+ "peerDependencies": {
46
+ "eslint": "^9.0.0"
47
+ }
48
+ }