@ai0x0/utils 0.0.34 → 0.0.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/eslint-rules/index.js +3 -1
- package/eslint-rules/max-lines.js +117 -0
- package/eslint-rules/no-hardcoded-style.js +112 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,19 +63,19 @@ export default [
|
|
|
63
63
|
];
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
| Rule | Description
|
|
67
|
-
| ------------------------- |
|
|
68
|
-
| `no-hardcoded-style` | Forbid hardcoded style values
|
|
69
|
-
| `no-one-letter-vars` | Forbid single-letter variable names
|
|
70
|
-
| `no-then` | Prefer `async/await` over `.then()`
|
|
71
|
-
| `no-antd-space` | Use `<Flex>` instead of `<Space>`
|
|
72
|
-
| `require-pro-components` | Require ProComponents usage
|
|
73
|
-
| `require-section-divider` | Large files need `===` comment dividers
|
|
74
|
-
| `no-consecutive-setstate` | Merge consecutive `setState` calls
|
|
75
|
-
| `no-use-request-run` | Forbid `useRequest.run()`
|
|
76
|
-
| `require-use-form` | Use `useForm()` for form state
|
|
77
|
-
| `require-form-convention` | Form naming conventions
|
|
78
|
-
| `max-lines` | Limit each file to 500 code lines
|
|
66
|
+
| Rule | Description |
|
|
67
|
+
| ------------------------- | ----------------------------------------------------------------------- |
|
|
68
|
+
| `no-hardcoded-style` | Forbid hardcoded style values |
|
|
69
|
+
| `no-one-letter-vars` | Forbid single-letter variable names |
|
|
70
|
+
| `no-then` | Prefer `async/await` over `.then()` |
|
|
71
|
+
| `no-antd-space` | Use `<Flex>` instead of `<Space>` |
|
|
72
|
+
| `require-pro-components` | Require ProComponents usage |
|
|
73
|
+
| `require-section-divider` | Large files need `===` comment dividers |
|
|
74
|
+
| `no-consecutive-setstate` | Merge consecutive `setState` calls |
|
|
75
|
+
| `no-use-request-run` | Forbid `useRequest.run()` |
|
|
76
|
+
| `require-use-form` | Use `useForm()` for form state |
|
|
77
|
+
| `require-form-convention` | Form naming conventions |
|
|
78
|
+
| `max-lines` | Limit each file to 500 code lines and prompt splitting into child files |
|
|
79
79
|
|
|
80
80
|
### Shared restrictions (`eslint-config/`)
|
|
81
81
|
|
package/eslint-rules/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import noUseRequestRun from "./no-use-request-run.js";
|
|
|
11
11
|
import noThen from "./no-then.js";
|
|
12
12
|
import noOneLetterVars from "./no-one-letter-vars.js";
|
|
13
13
|
import noConsecutiveSetState from "./no-consecutive-setstate.js";
|
|
14
|
+
import maxLines from "./max-lines.js";
|
|
14
15
|
|
|
15
16
|
const rules = {
|
|
16
17
|
"require-section-divider": requireSectionDivider,
|
|
@@ -23,6 +24,7 @@ const rules = {
|
|
|
23
24
|
"no-then": noThen,
|
|
24
25
|
"no-one-letter-vars": noOneLetterVars,
|
|
25
26
|
"no-consecutive-setstate": noConsecutiveSetState,
|
|
27
|
+
"max-lines": maxLines,
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
const plugin = {
|
|
@@ -35,7 +37,7 @@ const plugin = {
|
|
|
35
37
|
Object.keys(rules).map((name) => [`ai0x0/${name}`, "error"]),
|
|
36
38
|
),
|
|
37
39
|
curly: "error",
|
|
38
|
-
"max-lines": [
|
|
40
|
+
"ai0x0/max-lines": [
|
|
39
41
|
"error",
|
|
40
42
|
{ max: 500, skipBlankLines: true, skipComments: true },
|
|
41
43
|
],
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const DEFAULT_MAX = 500;
|
|
2
|
+
|
|
3
|
+
function normalizeOptions(options = {}) {
|
|
4
|
+
if (typeof options === "number") {
|
|
5
|
+
return {
|
|
6
|
+
max: options,
|
|
7
|
+
skipBlankLines: true,
|
|
8
|
+
skipComments: true,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
max: options.max ?? DEFAULT_MAX,
|
|
13
|
+
skipBlankLines: options.skipBlankLines ?? true,
|
|
14
|
+
skipComments: options.skipComments ?? true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getCommentLines(sourceCode) {
|
|
19
|
+
const lines = new Set();
|
|
20
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
21
|
+
const start = comment.loc.start.line;
|
|
22
|
+
const end = comment.loc.end.line;
|
|
23
|
+
for (let line = start; line <= end; line++) {
|
|
24
|
+
lines.add(line);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return lines;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
31
|
+
const rule = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: "layout",
|
|
34
|
+
docs: {
|
|
35
|
+
description: "限制单文件代码行数,超过时提示拆分子文件。",
|
|
36
|
+
},
|
|
37
|
+
schema: [
|
|
38
|
+
{
|
|
39
|
+
oneOf: [
|
|
40
|
+
{
|
|
41
|
+
type: "integer",
|
|
42
|
+
minimum: 0,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
max: {
|
|
49
|
+
type: "integer",
|
|
50
|
+
minimum: 0,
|
|
51
|
+
},
|
|
52
|
+
skipBlankLines: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
},
|
|
55
|
+
skipComments: {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
messages: {
|
|
64
|
+
tooManyLines:
|
|
65
|
+
"单文件代码行数过多({{ lineCount }} 行,最多 {{ max }} 行),请拆分为子文件。",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
create(context) {
|
|
69
|
+
const options = normalizeOptions(context.options[0]);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"Program:exit"(node) {
|
|
73
|
+
const sourceCode = context.getSourceCode();
|
|
74
|
+
const commentLines = options.skipComments
|
|
75
|
+
? getCommentLines(sourceCode)
|
|
76
|
+
: new Set();
|
|
77
|
+
let lineCount = 0;
|
|
78
|
+
let reportLine = sourceCode.lines.length || 1;
|
|
79
|
+
|
|
80
|
+
for (let index = 0; index < sourceCode.lines.length; index++) {
|
|
81
|
+
const lineNumber = index + 1;
|
|
82
|
+
const line = sourceCode.lines[index];
|
|
83
|
+
if (options.skipBlankLines && line.trim() === "") {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (options.skipComments && commentLines.has(lineNumber)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lineCount++;
|
|
91
|
+
if (lineCount === options.max + 1) {
|
|
92
|
+
reportLine = lineNumber;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (lineCount <= options.max) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
loc: {
|
|
103
|
+
line: reportLine,
|
|
104
|
+
column: 0,
|
|
105
|
+
},
|
|
106
|
+
messageId: "tooManyLines",
|
|
107
|
+
data: {
|
|
108
|
+
lineCount,
|
|
109
|
+
max: options.max,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export default rule;
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* no-hardcoded-style
|
|
3
|
-
* ------------------
|
|
4
|
-
* 禁止在 JSX style / createStyles / css`` 中直接写死样式值。
|
|
5
|
-
* 强制使用 antd token(theme.useToken() 或 createStyles(({token}) => ...))。
|
|
6
|
-
*
|
|
7
|
-
* 白名单:
|
|
8
|
-
* - token.xxx / token() / calc(...) 表达式
|
|
9
|
-
* - 0 值
|
|
10
|
-
* - CSS 功能性关键字(auto, none, hidden, pointer 等)
|
|
11
|
-
* - 百分比 / 带单位字符串
|
|
12
|
-
* - 条件/逻辑/变量/函数调用(无法静态判断)
|
|
13
|
-
* - css\`...\` 中的 \${token.xxx} 插值
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
1
|
// ==============================================================================
|
|
17
2
|
// 白名单
|
|
18
3
|
// ==============================================================================
|
|
@@ -22,6 +7,8 @@ const ALLOWED_LITERALS = new Set([
|
|
|
22
7
|
"none",
|
|
23
8
|
"hidden",
|
|
24
9
|
"visible",
|
|
10
|
+
"transparent",
|
|
11
|
+
"inherit",
|
|
25
12
|
"pre-wrap",
|
|
26
13
|
"pre",
|
|
27
14
|
"normal",
|
|
@@ -160,9 +147,6 @@ const TOKEN_KEYS = new Set([
|
|
|
160
147
|
"lineHeight",
|
|
161
148
|
"letterSpacing",
|
|
162
149
|
"boxShadow",
|
|
163
|
-
"textShadow",
|
|
164
|
-
"opacity",
|
|
165
|
-
"zIndex",
|
|
166
150
|
"width",
|
|
167
151
|
"height",
|
|
168
152
|
"minWidth",
|
|
@@ -207,6 +191,39 @@ function isTokenExpr(node) {
|
|
|
207
191
|
return false;
|
|
208
192
|
}
|
|
209
193
|
|
|
194
|
+
function containsTokenExpr(node) {
|
|
195
|
+
if (!node) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (isTokenExpr(node)) {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
switch (node.type) {
|
|
202
|
+
case "BinaryExpression":
|
|
203
|
+
case "LogicalExpression":
|
|
204
|
+
return containsTokenExpr(node.left) || containsTokenExpr(node.right);
|
|
205
|
+
case "UnaryExpression":
|
|
206
|
+
return containsTokenExpr(node.argument);
|
|
207
|
+
case "ConditionalExpression":
|
|
208
|
+
return [node.test, node.consequent, node.alternate].some((child) =>
|
|
209
|
+
containsTokenExpr(child),
|
|
210
|
+
);
|
|
211
|
+
case "CallExpression":
|
|
212
|
+
return [node.callee, ...node.arguments].some((child) =>
|
|
213
|
+
containsTokenExpr(child),
|
|
214
|
+
);
|
|
215
|
+
case "TemplateLiteral":
|
|
216
|
+
return node.expressions.some((expr) => containsTokenExpr(expr));
|
|
217
|
+
case "MemberExpression":
|
|
218
|
+
return containsTokenExpr(node.object) || containsTokenExpr(node.property);
|
|
219
|
+
default:
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const CSS_GLUE_KEYWORDS =
|
|
225
|
+
/\b(auto|none|hidden|visible|transparent|inherit|currentColor|important|calc|min|max|clamp|var|pre-wrap|pre|normal|pointer|default|move|not-allowed|grab|grabbing|center|flex-start|flex-end|space-between|space-around|left|right|top|bottom|start|end|solid|dashed|dotted|double|groove|ridge|inset|outset|block|inline-block|inline|flex|grid|contents|row|column|row-reverse|column-reverse|wrap|nowrap|wrap-reverse|relative|absolute|fixed|sticky|static|bold|bolder|lighter|italic|oblique|uppercase|lowercase|capitalize|underline|overline|line-through|blink|collapse|separate|border-box|content-box|padding-box|contain|cover|fill|scale-down)\b/g;
|
|
226
|
+
|
|
210
227
|
function isAllowedValue(node) {
|
|
211
228
|
if (!node) {
|
|
212
229
|
return true;
|
|
@@ -242,6 +259,7 @@ function isAllowedValue(node) {
|
|
|
242
259
|
"TemplateLiteral",
|
|
243
260
|
"CallExpression",
|
|
244
261
|
"Identifier",
|
|
262
|
+
"MemberExpression",
|
|
245
263
|
"SpreadElement",
|
|
246
264
|
].includes(node.type)
|
|
247
265
|
) {
|
|
@@ -253,6 +271,69 @@ function isAllowedValue(node) {
|
|
|
253
271
|
return false;
|
|
254
272
|
}
|
|
255
273
|
|
|
274
|
+
function normalizeCssValue(value) {
|
|
275
|
+
return value
|
|
276
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
277
|
+
.replace(/\s+/g, " ")
|
|
278
|
+
.trim();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isAllowedCssLiteralValue(value) {
|
|
282
|
+
const normalized = normalizeCssValue(value);
|
|
283
|
+
if (!normalized) {
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
if (
|
|
287
|
+
normalized === "0" ||
|
|
288
|
+
normalized === "0px" ||
|
|
289
|
+
ALLOWED_LITERALS.has(normalized)
|
|
290
|
+
) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (
|
|
294
|
+
/^\d+\.?\d*%$/.test(normalized) ||
|
|
295
|
+
/^100(vh|vw)$/.test(normalized) ||
|
|
296
|
+
normalized === "1"
|
|
297
|
+
) {
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function stripAllowedExpressionGlue(value) {
|
|
304
|
+
return value
|
|
305
|
+
.replace(/\$\{EXPR_\d+\}/g, " ")
|
|
306
|
+
.replace(/\b100(vh|vw)\b/g, " ")
|
|
307
|
+
.replace(/\b(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc)\b/g, " ")
|
|
308
|
+
.replace(CSS_GLUE_KEYWORDS, " ")
|
|
309
|
+
.replace(/\b0\b/g, " ")
|
|
310
|
+
.replace(/\b100\b/g, " ")
|
|
311
|
+
.replace(/[(),/+\-%!]/g, " ")
|
|
312
|
+
.replace(/\s+/g, " ")
|
|
313
|
+
.trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function hasHardcodedCssLiteral(value) {
|
|
317
|
+
const normalized = normalizeCssValue(value);
|
|
318
|
+
if (isAllowedCssLiteralValue(normalized)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
if (/#[0-9a-fA-F]{3,8}\b/.test(normalized)) {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
if (
|
|
325
|
+
/\b(?:rgb|rgba|hsl|hsla|oklch|oklab|linear-gradient|radial-gradient)\(/i.test(
|
|
326
|
+
normalized,
|
|
327
|
+
)
|
|
328
|
+
) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (/\b-?\d*\.?\d+\b/.test(stripAllowedExpressionGlue(normalized))) {
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
return stripAllowedExpressionGlue(normalized).length > 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
256
337
|
function checkStyleObject(context, objExpr) {
|
|
257
338
|
for (const prop of objExpr.properties) {
|
|
258
339
|
if (prop.type !== "Property") {
|
|
@@ -290,7 +371,7 @@ function checkCssTemplate(context, node) {
|
|
|
290
371
|
for (let i = 0; i < quasis.length; i++) {
|
|
291
372
|
fullText += quasis[i].value.raw;
|
|
292
373
|
if (i < expressions.length) {
|
|
293
|
-
fullText +=
|
|
374
|
+
fullText += `\${EXPR_${i}}`;
|
|
294
375
|
}
|
|
295
376
|
}
|
|
296
377
|
|
|
@@ -327,46 +408,25 @@ function checkCssTemplate(context, node) {
|
|
|
327
408
|
continue;
|
|
328
409
|
}
|
|
329
410
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
411
|
+
const expressionIndexes = [...rawVal.matchAll(/\$\{EXPR_(\d+)\}/g)].map(
|
|
412
|
+
(match) => Number(match[1]),
|
|
413
|
+
);
|
|
334
414
|
|
|
335
|
-
//
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// 纯百分比 → 允许(布局常用)
|
|
346
|
-
if (/^\d+\.?\d*%$/.test(rawVal)) {
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 100vh / 100vw → 允许(全屏布局)
|
|
351
|
-
if (/^100(vh|vw)$/.test(rawVal)) {
|
|
415
|
+
// 完全由非 token 变量 / 函数提供的值无法静态判断,保持兼容。
|
|
416
|
+
if (
|
|
417
|
+
expressionIndexes.length > 0 &&
|
|
418
|
+
expressionIndexes.every(
|
|
419
|
+
(index) => !containsTokenExpr(expressions[index]),
|
|
420
|
+
) &&
|
|
421
|
+
!hasHardcodedCssLiteral(rawVal)
|
|
422
|
+
) {
|
|
352
423
|
continue;
|
|
353
424
|
}
|
|
354
425
|
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
/^(\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc)|\d+\.?\d*%)$/.test(
|
|
358
|
-
rawVal,
|
|
359
|
-
)
|
|
360
|
-
) {
|
|
361
|
-
context.report({
|
|
362
|
-
node: template,
|
|
363
|
-
messageId: "noHardcoded",
|
|
364
|
-
data: { key: rawProp, value: rawVal },
|
|
365
|
-
});
|
|
426
|
+
if (!hasHardcodedCssLiteral(rawVal)) {
|
|
366
427
|
continue;
|
|
367
428
|
}
|
|
368
429
|
|
|
369
|
-
// 剩余 → 硬编码
|
|
370
430
|
context.report({
|
|
371
431
|
node: template,
|
|
372
432
|
messageId: "noHardcoded",
|
|
@@ -431,11 +491,6 @@ const rule = {
|
|
|
431
491
|
}
|
|
432
492
|
if (prop.value.type === "ObjectExpression") {
|
|
433
493
|
checkStyleObject(context, prop.value);
|
|
434
|
-
} else if (
|
|
435
|
-
prop.value.type === "TaggedTemplateExpression" &&
|
|
436
|
-
prop.value.tag.name === "css"
|
|
437
|
-
) {
|
|
438
|
-
checkCssTemplate(context, prop.value);
|
|
439
494
|
}
|
|
440
495
|
}
|
|
441
496
|
return;
|
|
@@ -458,11 +513,6 @@ const rule = {
|
|
|
458
513
|
}
|
|
459
514
|
if (prop.value.type === "ObjectExpression") {
|
|
460
515
|
checkStyleObject(context, prop.value);
|
|
461
|
-
} else if (
|
|
462
|
-
prop.value.type === "TaggedTemplateExpression" &&
|
|
463
|
-
prop.value.tag.name === "css"
|
|
464
|
-
) {
|
|
465
|
-
checkCssTemplate(context, prop.value);
|
|
466
516
|
}
|
|
467
517
|
}
|
|
468
518
|
return;
|
|
@@ -480,11 +530,6 @@ const rule = {
|
|
|
480
530
|
}
|
|
481
531
|
if (prop.value.type === "ObjectExpression") {
|
|
482
532
|
checkStyleObject(context, prop.value);
|
|
483
|
-
} else if (
|
|
484
|
-
prop.value.type === "TaggedTemplateExpression" &&
|
|
485
|
-
prop.value.tag.name === "css"
|
|
486
|
-
) {
|
|
487
|
-
checkCssTemplate(context, prop.value);
|
|
488
533
|
}
|
|
489
534
|
}
|
|
490
535
|
}
|