@ai0x0/utils 0.0.32 → 0.0.33
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/.agents/skills/ai0x0-utils/backend/SKILL.md +366 -0
- package/.agents/skills/antd/SKILL.md +255 -0
- package/.agents/skills/cloudflare/SKILL.md +132 -0
- package/.agents/skills/nextjs/SKILL.md +252 -0
- package/README.md +34 -0
- package/eslint-rules/no-hardcoded-style.js +103 -42
- package/eslint-rules/require-section-divider.js +30 -11
- package/eslint-rules/require-use-form.js +12 -5
- package/package.json +2 -1
|
@@ -187,41 +187,52 @@ for (const key of TOKEN_KEYS) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function isTokenExpr(node) {
|
|
190
|
-
if (!node)
|
|
190
|
+
if (!node) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
191
193
|
if (
|
|
192
194
|
node.type === "MemberExpression" &&
|
|
193
195
|
node.object.type === "Identifier" &&
|
|
194
196
|
node.object.name === "token"
|
|
195
|
-
)
|
|
197
|
+
) {
|
|
196
198
|
return true;
|
|
199
|
+
}
|
|
197
200
|
if (
|
|
198
201
|
node.type === "CallExpression" &&
|
|
199
202
|
node.callee.type === "Identifier" &&
|
|
200
203
|
["token", "calc", "max", "min", "clamp"].includes(node.callee.name)
|
|
201
|
-
)
|
|
204
|
+
) {
|
|
202
205
|
return true;
|
|
206
|
+
}
|
|
203
207
|
return false;
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
function isAllowedValue(node) {
|
|
207
|
-
if (!node)
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
if (!node) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (isTokenExpr(node)) {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (node.type === "Literal" && (node.value === 0 || node.value === "0")) {
|
|
210
218
|
return true;
|
|
219
|
+
}
|
|
211
220
|
if (
|
|
212
221
|
node.type === "Literal" &&
|
|
213
222
|
typeof node.value === "string" &&
|
|
214
223
|
ALLOWED_LITERALS.has(node.value)
|
|
215
|
-
)
|
|
224
|
+
) {
|
|
216
225
|
return true;
|
|
226
|
+
}
|
|
217
227
|
if (
|
|
218
228
|
node.type === "Literal" &&
|
|
219
229
|
typeof node.value === "string" &&
|
|
220
230
|
/^(\d+(\.\d+)?(px|em|rem|%|vh|vw|vmin|vmax|ex|ch|cm|mm|in|pt|pc)|\d+(\.\d+)?\s+(px|em|rem)\s+(solid|dashed|dotted)|\d+\.?\d*%)$/.test(
|
|
221
231
|
node.value,
|
|
222
232
|
)
|
|
223
|
-
)
|
|
233
|
+
) {
|
|
224
234
|
return true;
|
|
235
|
+
}
|
|
225
236
|
if (
|
|
226
237
|
[
|
|
227
238
|
"ConditionalExpression",
|
|
@@ -233,25 +244,33 @@ function isAllowedValue(node) {
|
|
|
233
244
|
"Identifier",
|
|
234
245
|
"SpreadElement",
|
|
235
246
|
].includes(node.type)
|
|
236
|
-
)
|
|
247
|
+
) {
|
|
237
248
|
return true;
|
|
238
|
-
|
|
249
|
+
}
|
|
250
|
+
if (node.type === "ObjectExpression" || node.type === "ArrayExpression") {
|
|
239
251
|
return true;
|
|
252
|
+
}
|
|
240
253
|
return false;
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
function checkStyleObject(context, objExpr) {
|
|
244
257
|
for (const prop of objExpr.properties) {
|
|
245
|
-
if (prop.type !== "Property")
|
|
258
|
+
if (prop.type !== "Property") {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
246
261
|
const key =
|
|
247
262
|
prop.key.type === "Identifier"
|
|
248
263
|
? prop.key.name
|
|
249
264
|
: prop.key.type === "Literal"
|
|
250
265
|
? prop.key.value
|
|
251
266
|
: null;
|
|
252
|
-
if (!key || !TOKEN_KEYS.has(key))
|
|
267
|
+
if (!key || !TOKEN_KEYS.has(key)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
253
270
|
const val = prop.value;
|
|
254
|
-
if (isAllowedValue(val))
|
|
271
|
+
if (isAllowedValue(val)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
255
274
|
const valText = context.getSourceCode().getText(val);
|
|
256
275
|
context.report({
|
|
257
276
|
node: prop,
|
|
@@ -270,7 +289,9 @@ function checkCssTemplate(context, node) {
|
|
|
270
289
|
let fullText = "";
|
|
271
290
|
for (let i = 0; i < quasis.length; i++) {
|
|
272
291
|
fullText += quasis[i].value.raw;
|
|
273
|
-
if (i < expressions.length)
|
|
292
|
+
if (i < expressions.length) {
|
|
293
|
+
fullText += "${EXPR}";
|
|
294
|
+
}
|
|
274
295
|
}
|
|
275
296
|
|
|
276
297
|
// 按行分割,逐行检查
|
|
@@ -278,7 +299,9 @@ function checkCssTemplate(context, node) {
|
|
|
278
299
|
for (const line of lines) {
|
|
279
300
|
// 简单解析:找冒号分割的 property: value
|
|
280
301
|
const colonIdx = line.indexOf(":");
|
|
281
|
-
if (colonIdx === -1)
|
|
302
|
+
if (colonIdx === -1) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
282
305
|
|
|
283
306
|
const rawProp = line.slice(0, colonIdx).trim();
|
|
284
307
|
const rawVal = line
|
|
@@ -292,29 +315,42 @@ function checkCssTemplate(context, node) {
|
|
|
292
315
|
rawProp.startsWith("&") ||
|
|
293
316
|
rawProp.startsWith("@") ||
|
|
294
317
|
rawProp.startsWith("//")
|
|
295
|
-
)
|
|
318
|
+
) {
|
|
296
319
|
continue;
|
|
320
|
+
}
|
|
297
321
|
|
|
298
322
|
// 匹配 camelCase 或 kebab-case 属性
|
|
299
323
|
const camelProp = TOKEN_KEYS.has(rawProp)
|
|
300
324
|
? rawProp
|
|
301
325
|
: KEBAB_TO_CAMEL[rawProp];
|
|
302
|
-
if (!camelProp)
|
|
326
|
+
if (!camelProp) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
303
329
|
|
|
304
330
|
// 值包含 ${EXPR} → 被插值打断或完全由表达式提供 → 跳过
|
|
305
|
-
if (rawVal.includes("${EXPR}"))
|
|
331
|
+
if (rawVal.includes("${EXPR}")) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
306
334
|
|
|
307
335
|
// 0 值
|
|
308
|
-
if (rawVal === "0" || rawVal === "0px")
|
|
336
|
+
if (rawVal === "0" || rawVal === "0px") {
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
309
339
|
|
|
310
340
|
// CSS 关键字白名单
|
|
311
|
-
if (ALLOWED_LITERALS.has(rawVal))
|
|
341
|
+
if (ALLOWED_LITERALS.has(rawVal)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
312
344
|
|
|
313
345
|
// 纯百分比 → 允许(布局常用)
|
|
314
|
-
if (/^\d+\.?\d*%$/.test(rawVal))
|
|
346
|
+
if (/^\d+\.?\d*%$/.test(rawVal)) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
315
349
|
|
|
316
350
|
// 100vh / 100vw → 允许(全屏布局)
|
|
317
|
-
if (/^100(vh|vw)$/.test(rawVal))
|
|
351
|
+
if (/^100(vh|vw)$/.test(rawVal)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
318
354
|
|
|
319
355
|
// 百分比 / 带单位字符串 → 硬编码(token 体系也有 sizeStep/sizeUnit)
|
|
320
356
|
if (
|
|
@@ -339,6 +375,10 @@ function checkCssTemplate(context, node) {
|
|
|
339
375
|
}
|
|
340
376
|
}
|
|
341
377
|
|
|
378
|
+
// ==============================================================================
|
|
379
|
+
// ESLint 规则
|
|
380
|
+
// ==============================================================================
|
|
381
|
+
|
|
342
382
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
343
383
|
const rule = {
|
|
344
384
|
meta: {
|
|
@@ -350,15 +390,19 @@ const rule = {
|
|
|
350
390
|
schema: [],
|
|
351
391
|
messages: {
|
|
352
392
|
noHardcoded:
|
|
353
|
-
"禁止硬编码 `{{ {{key}}: {{value}} }}`。请使用 antd token(如 `token.marginSM`、`token.colorPrimary`)。可参考
|
|
393
|
+
"禁止硬编码 `{{ {{key}}: {{value}} }}`。请使用 antd token(如 `token.marginSM`、`token.colorPrimary`)。可参考 node_modules/@ai0x0/utils/.agents/skills/antd/SKILL.md 了解 token 体系。",
|
|
354
394
|
},
|
|
355
395
|
},
|
|
356
396
|
create(context) {
|
|
357
397
|
return {
|
|
358
398
|
JSXAttribute(node) {
|
|
359
|
-
if (node.name.name !== "style")
|
|
399
|
+
if (node.name.name !== "style") {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
360
402
|
const expr = node.value?.expression;
|
|
361
|
-
if (!expr || expr.type !== "ObjectExpression")
|
|
403
|
+
if (!expr || expr.type !== "ObjectExpression") {
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
362
406
|
checkStyleObject(context, expr);
|
|
363
407
|
},
|
|
364
408
|
|
|
@@ -372,40 +416,54 @@ const rule = {
|
|
|
372
416
|
if (
|
|
373
417
|
node.callee.type !== "Identifier" ||
|
|
374
418
|
node.callee.name !== "createStyles"
|
|
375
|
-
)
|
|
419
|
+
) {
|
|
376
420
|
return;
|
|
421
|
+
}
|
|
377
422
|
const arg = node.arguments[0];
|
|
378
|
-
if (!arg)
|
|
423
|
+
if (!arg) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
379
426
|
|
|
380
427
|
if (arg.type === "ObjectExpression") {
|
|
381
428
|
for (const prop of arg.properties) {
|
|
382
|
-
if (prop.type !== "Property")
|
|
383
|
-
|
|
429
|
+
if (prop.type !== "Property") {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (prop.value.type === "ObjectExpression") {
|
|
384
433
|
checkStyleObject(context, prop.value);
|
|
385
|
-
else if (
|
|
434
|
+
} else if (
|
|
386
435
|
prop.value.type === "TaggedTemplateExpression" &&
|
|
387
436
|
prop.value.tag.name === "css"
|
|
388
|
-
)
|
|
437
|
+
) {
|
|
389
438
|
checkCssTemplate(context, prop.value);
|
|
439
|
+
}
|
|
390
440
|
}
|
|
391
441
|
return;
|
|
392
442
|
}
|
|
393
443
|
|
|
394
444
|
let body = null;
|
|
395
|
-
if (arg.type === "ArrowFunctionExpression")
|
|
396
|
-
|
|
397
|
-
if (
|
|
445
|
+
if (arg.type === "ArrowFunctionExpression") {
|
|
446
|
+
body = arg.body;
|
|
447
|
+
} else if (arg.type === "FunctionExpression") {
|
|
448
|
+
body = arg.body;
|
|
449
|
+
}
|
|
450
|
+
if (!body) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
398
453
|
|
|
399
454
|
if (body.type === "ObjectExpression") {
|
|
400
455
|
for (const prop of body.properties) {
|
|
401
|
-
if (prop.type !== "Property")
|
|
402
|
-
|
|
456
|
+
if (prop.type !== "Property") {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (prop.value.type === "ObjectExpression") {
|
|
403
460
|
checkStyleObject(context, prop.value);
|
|
404
|
-
else if (
|
|
461
|
+
} else if (
|
|
405
462
|
prop.value.type === "TaggedTemplateExpression" &&
|
|
406
463
|
prop.value.tag.name === "css"
|
|
407
|
-
)
|
|
464
|
+
) {
|
|
408
465
|
checkCssTemplate(context, prop.value);
|
|
466
|
+
}
|
|
409
467
|
}
|
|
410
468
|
return;
|
|
411
469
|
}
|
|
@@ -417,14 +475,17 @@ const rule = {
|
|
|
417
475
|
stmt.argument?.type === "ObjectExpression"
|
|
418
476
|
) {
|
|
419
477
|
for (const prop of stmt.argument.properties) {
|
|
420
|
-
if (prop.type !== "Property")
|
|
421
|
-
|
|
478
|
+
if (prop.type !== "Property") {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (prop.value.type === "ObjectExpression") {
|
|
422
482
|
checkStyleObject(context, prop.value);
|
|
423
|
-
else if (
|
|
483
|
+
} else if (
|
|
424
484
|
prop.value.type === "TaggedTemplateExpression" &&
|
|
425
485
|
prop.value.tag.name === "css"
|
|
426
|
-
)
|
|
486
|
+
) {
|
|
427
487
|
checkCssTemplate(context, prop.value);
|
|
488
|
+
}
|
|
428
489
|
}
|
|
429
490
|
}
|
|
430
491
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - 550 ~ 749 行:至少 3 个分区块
|
|
10
10
|
* 以此类推,每 200 行增加 1 个分区块。
|
|
11
11
|
*
|
|
12
|
-
* 分隔注释块格式(见
|
|
12
|
+
* 分隔注释块格式(见 node_modules/@ai0x0/utils/.agents/skills/nextjs/SKILL.md):
|
|
13
13
|
*
|
|
14
14
|
* // ==============================================================================
|
|
15
15
|
* // <中文标题>
|
|
@@ -22,24 +22,39 @@ const DIVIDER_EQ = /^\s*={4,}\s*$/;
|
|
|
22
22
|
|
|
23
23
|
/** 相邻两行注释 + 之间中间一行文字:判定一个三行注释块为分隔块。 */
|
|
24
24
|
function isDividerBlock(commentA, commentB, commentC) {
|
|
25
|
-
if (!commentA || !commentB || !commentC)
|
|
25
|
+
if (!commentA || !commentB || !commentC) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
26
28
|
if (
|
|
27
29
|
commentA.type !== "Line" ||
|
|
28
30
|
commentB.type !== "Line" ||
|
|
29
31
|
commentC.type !== "Line"
|
|
30
|
-
)
|
|
32
|
+
) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!DIVIDER_EQ.test(commentA.value)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (!DIVIDER_EQ.test(commentC.value)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (!commentB.value.trim()) {
|
|
31
42
|
return false;
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (commentB.loc.end.line + 1 !== commentC.loc.start.line)
|
|
43
|
+
}
|
|
44
|
+
if (commentA.loc.end.line + 1 !== commentB.loc.start.line) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (commentB.loc.end.line + 1 !== commentC.loc.start.line) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
37
50
|
return true;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/** 计算文件需要多少个分区块(每 150 行 1 个,从 150 行起)。 */
|
|
41
54
|
function requiredDividers(totalLines, minLines) {
|
|
42
|
-
if (totalLines < minLines)
|
|
55
|
+
if (totalLines < minLines) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
43
58
|
// 150-299: 1个, 300-449: 2个, 依此类推
|
|
44
59
|
return Math.floor(totalLines / minLines);
|
|
45
60
|
}
|
|
@@ -74,10 +89,14 @@ const rule = {
|
|
|
74
89
|
return {
|
|
75
90
|
Program(program) {
|
|
76
91
|
const totalLines = program.loc.end.line;
|
|
77
|
-
if (totalLines < minLines)
|
|
92
|
+
if (totalLines < minLines) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
const required = requiredDividers(totalLines, minLines);
|
|
80
|
-
if (required <= 0)
|
|
97
|
+
if (required <= 0) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
81
100
|
|
|
82
101
|
const allComments = sourceCode.getAllComments();
|
|
83
102
|
|
|
@@ -23,7 +23,7 @@ const rule = {
|
|
|
23
23
|
schema: [],
|
|
24
24
|
messages: {
|
|
25
25
|
useFormRequired:
|
|
26
|
-
"检测到表单场景使用了 useState/useSetState 管理字段值。请改用 Form.useForm() 或 rc-field-form 的 useForm。详见
|
|
26
|
+
"检测到表单场景使用了 useState/useSetState 管理字段值。请改用 Form.useForm() 或 rc-field-form 的 useForm。详见 node_modules/@ai0x0/utils/.agents/skills/antd/SKILL.md。",
|
|
27
27
|
},
|
|
28
28
|
},
|
|
29
29
|
create(context) {
|
|
@@ -39,16 +39,23 @@ const rule = {
|
|
|
39
39
|
|
|
40
40
|
return {
|
|
41
41
|
JSXOpeningElement(node) {
|
|
42
|
-
if (hasFormPattern)
|
|
42
|
+
if (hasFormPattern) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
let hasValue = false;
|
|
45
47
|
let hasOnChange = false;
|
|
46
48
|
|
|
47
49
|
for (const attr of node.attributes) {
|
|
48
|
-
if (attr.type !== "JSXAttribute" || !attr.name)
|
|
50
|
+
if (attr.type !== "JSXAttribute" || !attr.name) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
49
53
|
const attrName = attr.name.name;
|
|
50
|
-
if (attrName === "value")
|
|
51
|
-
|
|
54
|
+
if (attrName === "value") {
|
|
55
|
+
hasValue = true;
|
|
56
|
+
} else if (attrName === "onChange") {
|
|
57
|
+
hasOnChange = true;
|
|
58
|
+
}
|
|
52
59
|
|
|
53
60
|
if (hasValue && hasOnChange) {
|
|
54
61
|
hasFormPattern = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai0x0/utils",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"description": "AI0x0 utils",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai0x0"
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"lib",
|
|
25
25
|
"eslint-config",
|
|
26
26
|
"eslint-rules",
|
|
27
|
+
".agents",
|
|
27
28
|
"SKILL.md"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|