@lark-apaas/fullstack-presets 1.1.5-beta.20 → 1.1.5-beta.22
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/lib/custom-eslint-rules/no-welcome-index-route.d.ts +12 -0
- package/lib/custom-eslint-rules/no-welcome-index-route.js +123 -0
- package/lib/custom-eslint-rules/require-index-route.d.ts +14 -0
- package/lib/custom-eslint-rules/require-index-route.js +98 -0
- package/lib/custom-stylelint-rules/hsl-variable.d.ts +7 -0
- package/lib/custom-stylelint-rules/hsl-variable.js +90 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -21,6 +21,13 @@
|
|
|
21
21
|
* 跳过(不检查)的示例:
|
|
22
22
|
* --shadow: 0px 2px 12px hsl(0 0% 0% / 0.01); // 复合值中的 hsl
|
|
23
23
|
* background: linear-gradient(hsl(...), ...); // 复合值中的 hsl
|
|
24
|
+
* --border: hsl(from hsl(217 80% 55%) h s calc(l + var(--x))); // CSS Color Level 5 相对颜色语法
|
|
25
|
+
* --color: hsl(none 50% 26%); // none 关键字(CSS Color Level 4)
|
|
26
|
+
*
|
|
27
|
+
* var() 通道的处理:
|
|
28
|
+
* 对每个 var() 独立尝试占位符 0 和 0%(hue 位置需要数字,sat/light 位置需要百分比),
|
|
29
|
+
* 穷举所有 2^n 组合,只要存在一种合法结构就认为该写法合法。
|
|
30
|
+
* 这样可以在不误报合法写法的前提下,仍然捕获结构性错误(如缺少参数)。
|
|
24
31
|
*/
|
|
25
32
|
import stylelint from 'stylelint';
|
|
26
33
|
export declare const ruleName = "custom/hsl-valid-value";
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.meta = exports.messages = exports.ruleName = void 0;
|
|
40
|
+
// @ts-nocheck
|
|
40
41
|
/**
|
|
41
42
|
* 自定义 Stylelint 规则:校验 CSS 声明中非法的 hsl() 写法
|
|
42
43
|
*
|
|
@@ -60,6 +61,13 @@ exports.meta = exports.messages = exports.ruleName = void 0;
|
|
|
60
61
|
* 跳过(不检查)的示例:
|
|
61
62
|
* --shadow: 0px 2px 12px hsl(0 0% 0% / 0.01); // 复合值中的 hsl
|
|
62
63
|
* background: linear-gradient(hsl(...), ...); // 复合值中的 hsl
|
|
64
|
+
* --border: hsl(from hsl(217 80% 55%) h s calc(l + var(--x))); // CSS Color Level 5 相对颜色语法
|
|
65
|
+
* --color: hsl(none 50% 26%); // none 关键字(CSS Color Level 4)
|
|
66
|
+
*
|
|
67
|
+
* var() 通道的处理:
|
|
68
|
+
* 对每个 var() 独立尝试占位符 0 和 0%(hue 位置需要数字,sat/light 位置需要百分比),
|
|
69
|
+
* 穷举所有 2^n 组合,只要存在一种合法结构就认为该写法合法。
|
|
70
|
+
* 这样可以在不误报合法写法的前提下,仍然捕获结构性错误(如缺少参数)。
|
|
63
71
|
*/
|
|
64
72
|
const stylelint_1 = __importDefault(require("stylelint"));
|
|
65
73
|
const csstree = __importStar(require("css-tree"));
|
|
@@ -71,6 +79,65 @@ exports.messages = ruleMessages(exports.ruleName, {
|
|
|
71
79
|
exports.meta = {
|
|
72
80
|
url: 'https://github.com/csstree/stylelint-validator',
|
|
73
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* 提取字符串中所有顶层 var() 的位置和长度,使用括号计数支持任意深度嵌套。
|
|
84
|
+
* 例如 var(--a, var(--b, var(--c, 0))) 会被作为一个整体提取。
|
|
85
|
+
*/
|
|
86
|
+
function extractVarRanges(value) {
|
|
87
|
+
const ranges = [];
|
|
88
|
+
const varStart = /var\s*\(/gi;
|
|
89
|
+
let m;
|
|
90
|
+
while ((m = varStart.exec(value)) !== null) {
|
|
91
|
+
let depth = 1;
|
|
92
|
+
let j = m.index + m[0].length;
|
|
93
|
+
while (j < value.length && depth > 0) {
|
|
94
|
+
if (value[j] === '(')
|
|
95
|
+
depth++;
|
|
96
|
+
else if (value[j] === ')')
|
|
97
|
+
depth--;
|
|
98
|
+
j++;
|
|
99
|
+
}
|
|
100
|
+
ranges.push({ index: m.index, length: j - m.index });
|
|
101
|
+
varStart.lastIndex = j; // 跳过已处理的 var(),避免重复匹配内层
|
|
102
|
+
}
|
|
103
|
+
return ranges;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 对 hsl() 值中的每个 var() 分别尝试占位符 "0" 和 "0%",
|
|
107
|
+
* 穷举 2^n 种组合,只要任意一种通过 css-tree 词法验证就返回 true。
|
|
108
|
+
*
|
|
109
|
+
* 背景:css-tree 不能解析 var(),但不同通道对占位符类型要求不同:
|
|
110
|
+
* - hue 位置需要数字(0),saturation/lightness 位置需要百分比(0%)。
|
|
111
|
+
* 穷举组合避免了需要感知通道位置的复杂解析。
|
|
112
|
+
*/
|
|
113
|
+
function isHslValidWithVarPlaceholders(value, ranges) {
|
|
114
|
+
const isValidCssColor = (v) => {
|
|
115
|
+
try {
|
|
116
|
+
const ast = csstree.parse(v, { context: 'value' });
|
|
117
|
+
return !csstree.lexer.matchType('color', ast).error;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const n = ranges.length;
|
|
124
|
+
const total = 1 << n; // 2^n 种组合
|
|
125
|
+
for (let mask = 0; mask < total; mask++) {
|
|
126
|
+
let replaced = value;
|
|
127
|
+
let offset = 0;
|
|
128
|
+
for (let i = 0; i < n; i++) {
|
|
129
|
+
const placeholder = (mask >> i) & 1 ? '0%' : '0';
|
|
130
|
+
const m = ranges[i];
|
|
131
|
+
const start = m.index + offset;
|
|
132
|
+
const end = start + m.length;
|
|
133
|
+
replaced = replaced.slice(0, start) + placeholder + replaced.slice(end);
|
|
134
|
+
offset += placeholder.length - m.length;
|
|
135
|
+
}
|
|
136
|
+
if (isValidCssColor(replaced))
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false; // 所有组合均非法,视为结构性错误
|
|
140
|
+
}
|
|
74
141
|
const rule = primary => {
|
|
75
142
|
return (root, result) => {
|
|
76
143
|
const validOptions = validateOptions(result, exports.ruleName, {
|
|
@@ -84,6 +151,29 @@ const rule = primary => {
|
|
|
84
151
|
// 只处理纯 hsl/hsla 颜色值,跳过复合值(如 box-shadow)
|
|
85
152
|
if (!/^hsla?\(/i.test(value))
|
|
86
153
|
return;
|
|
154
|
+
// 跳过 CSS Color Level 5 相对颜色语法:hsl(from <color> h s l)
|
|
155
|
+
// css-tree 词法器尚不支持该语法,直接跳过避免误报
|
|
156
|
+
if (/^hsla?\(\s*from\s+/i.test(value))
|
|
157
|
+
return;
|
|
158
|
+
// none 关键字只在现代空格语法中合法(CSS Color Level 4),逗号语法不支持
|
|
159
|
+
// 通过是否含逗号区分语法:现代语法跳过,逗号语法交由 css-tree 验证
|
|
160
|
+
const isLegacySyntax = /^hsla?\s*\([^)]*,/i.test(value);
|
|
161
|
+
if (!isLegacySyntax && /\bnone\b/i.test(value))
|
|
162
|
+
return;
|
|
163
|
+
// 包含 var() 时,穷举占位符组合进行结构性校验
|
|
164
|
+
const varRanges = extractVarRanges(value);
|
|
165
|
+
if (varRanges.length > 0) {
|
|
166
|
+
if (!isHslValidWithVarPlaceholders(value, varRanges)) {
|
|
167
|
+
report({
|
|
168
|
+
message: exports.messages.invalidHsl(value),
|
|
169
|
+
node: decl,
|
|
170
|
+
result,
|
|
171
|
+
ruleName: exports.ruleName,
|
|
172
|
+
word: value,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
87
177
|
try {
|
|
88
178
|
const ast = csstree.parse(value, { context: 'value' });
|
|
89
179
|
const matchResult = csstree.lexer.matchType('color', ast);
|