king-design-analyzer 1.0.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.
Files changed (92) hide show
  1. package/README.md +151 -0
  2. package/components/affix.json +178 -0
  3. package/components/az.json +86 -0
  4. package/components/badge.json +162 -0
  5. package/components/billtypes.json +90 -0
  6. package/components/breadcrumb.json +220 -0
  7. package/components/button.json +511 -0
  8. package/components/card.json +334 -0
  9. package/components/cardcontent.json +144 -0
  10. package/components/carousel.json +207 -0
  11. package/components/cascader.json +373 -0
  12. package/components/checkbox.json +298 -0
  13. package/components/code.json +318 -0
  14. package/components/collapse.json +358 -0
  15. package/components/copy.json +220 -0
  16. package/components/datepicker.json +529 -0
  17. package/components/descriptions.json +309 -0
  18. package/components/dialog.json +460 -0
  19. package/components/divider.json +287 -0
  20. package/components/drawer.json +563 -0
  21. package/components/dropdown.json +407 -0
  22. package/components/editable.json +331 -0
  23. package/components/ellipsis.json +218 -0
  24. package/components/form.json +638 -0
  25. package/components/grid.json +377 -0
  26. package/components/icon.json +442 -0
  27. package/components/input.json +821 -0
  28. package/components/layoutcontent.json +210 -0
  29. package/components/menu.json +482 -0
  30. package/components/message.json +345 -0
  31. package/components/pagination.json +444 -0
  32. package/components/paginationplus.json +74 -0
  33. package/components/popover.json +456 -0
  34. package/components/progress.json +354 -0
  35. package/components/protable.json +132 -0
  36. package/components/radio.json +246 -0
  37. package/components/region.json +115 -0
  38. package/components/select.json +676 -0
  39. package/components/slider.json +422 -0
  40. package/components/spin.json +232 -0
  41. package/components/spinner.json +441 -0
  42. package/components/status.json +75 -0
  43. package/components/steps.json +292 -0
  44. package/components/switch.json +357 -0
  45. package/components/table.json +1057 -0
  46. package/components/tablecolumnid.json +217 -0
  47. package/components/tabs.json +363 -0
  48. package/components/tag.json +504 -0
  49. package/components/timepicker.json +447 -0
  50. package/components/tip.json +322 -0
  51. package/components/tooltip.json +416 -0
  52. package/components/tour.json +395 -0
  53. package/components/transfer.json +414 -0
  54. package/components/tree.json +480 -0
  55. package/components/treeselect.json +478 -0
  56. package/components/upload.json +499 -0
  57. package/components/virtuallist.json +178 -0
  58. package/dist/ast/index.d.mts +71 -0
  59. package/dist/ast/index.d.ts +71 -0
  60. package/dist/ast/index.js +15 -0
  61. package/dist/ast/index.mjs +2 -0
  62. package/dist/chunk-4BUGNH4F.mjs +62 -0
  63. package/dist/chunk-5H7N2A5X.mjs +1 -0
  64. package/dist/chunk-5IF32MBB.js +603 -0
  65. package/dist/chunk-ARWRNWDW.js +219 -0
  66. package/dist/chunk-C3L4IXJC.mjs +577 -0
  67. package/dist/chunk-GNVCC37B.js +102 -0
  68. package/dist/chunk-N4UIA6DN.js +68 -0
  69. package/dist/chunk-RAMIBZAU.mjs +216 -0
  70. package/dist/chunk-RNQHI7YG.js +64 -0
  71. package/dist/chunk-TB6BF5TJ.mjs +99 -0
  72. package/dist/chunk-UX7KGX45.mjs +66 -0
  73. package/dist/chunk-YTEYDSDW.js +2 -0
  74. package/dist/full/index.d.mts +29 -0
  75. package/dist/full/index.d.ts +29 -0
  76. package/dist/full/index.js +16 -0
  77. package/dist/full/index.mjs +3 -0
  78. package/dist/index.d.mts +5 -0
  79. package/dist/index.d.ts +5 -0
  80. package/dist/index.js +43 -0
  81. package/dist/index.mjs +6 -0
  82. package/dist/runtime/index.d.mts +14 -0
  83. package/dist/runtime/index.d.ts +14 -0
  84. package/dist/runtime/index.js +15 -0
  85. package/dist/runtime/index.mjs +2 -0
  86. package/dist/sfcCompiler-m51JOfWs.d.mts +22 -0
  87. package/dist/sfcCompiler-m51JOfWs.d.ts +22 -0
  88. package/dist/static/index.d.mts +14 -0
  89. package/dist/static/index.d.ts +14 -0
  90. package/dist/static/index.js +19 -0
  91. package/dist/static/index.mjs +2 -0
  92. package/package.json +88 -0
@@ -0,0 +1,577 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as fsSync from 'fs';
3
+ import * as path from 'path';
4
+ import { parse } from '@vue/compiler-sfc';
5
+ import * as ts from 'typescript';
6
+
7
+ // src/analysis/componentRegistry.ts
8
+ var ComponentRegistry = class {
9
+ constructor() {
10
+ this.components = /* @__PURE__ */ new Map();
11
+ this.loaded = false;
12
+ this.watcher = null;
13
+ // Hardcoded path to metadata in this specific workspace environment
14
+ this.metadataPath = path.join(process.cwd(), "components");
15
+ }
16
+ async load() {
17
+ if (this.loaded) return;
18
+ await this.reload();
19
+ this.loaded = true;
20
+ this.startWatching();
21
+ }
22
+ /**
23
+ * 重新加载所有组件元数据
24
+ */
25
+ async reload() {
26
+ const newComponents = /* @__PURE__ */ new Map();
27
+ try {
28
+ const files = await fs.readdir(this.metadataPath);
29
+ for (const file of files) {
30
+ if (!file.endsWith(".json")) continue;
31
+ const content = await fs.readFile(path.join(this.metadataPath, file), "utf-8");
32
+ const data = JSON.parse(content);
33
+ newComponents.set(data.name, data);
34
+ if (data.subComponents) {
35
+ for (const sub of data.subComponents) {
36
+ newComponents.set(sub.name, sub);
37
+ }
38
+ }
39
+ }
40
+ this.components = newComponents;
41
+ console.log(`[ComponentRegistry] ${this.loaded ? "Reloaded" : "Loaded"} ${this.components.size} components.`);
42
+ } catch (error) {
43
+ console.error("[ComponentRegistry] Failed to load metadata:", error);
44
+ }
45
+ }
46
+ /**
47
+ * 开始监听文件变化
48
+ */
49
+ startWatching() {
50
+ if (this.watcher) return;
51
+ try {
52
+ let reloadTimeout = null;
53
+ this.watcher = fsSync.watch(this.metadataPath, (eventType, filename) => {
54
+ if (!filename?.endsWith(".json")) return;
55
+ console.log(`[ComponentRegistry] Detected change: ${filename} (${eventType})`);
56
+ if (reloadTimeout) {
57
+ clearTimeout(reloadTimeout);
58
+ }
59
+ reloadTimeout = setTimeout(() => {
60
+ this.reload();
61
+ }, 500);
62
+ });
63
+ console.log(`[ComponentRegistry] Hot reload enabled for: ${this.metadataPath}`);
64
+ } catch (error) {
65
+ console.warn("[ComponentRegistry] Could not enable hot reload:", error);
66
+ }
67
+ }
68
+ /**
69
+ * 停止监听
70
+ */
71
+ stopWatching() {
72
+ if (this.watcher) {
73
+ this.watcher.close();
74
+ this.watcher = null;
75
+ console.log("[ComponentRegistry] Hot reload disabled.");
76
+ }
77
+ }
78
+ getComponent(name) {
79
+ return this.components.get(name);
80
+ }
81
+ getAllComponentNames() {
82
+ return Array.from(this.components.keys());
83
+ }
84
+ isKnownComponent(name) {
85
+ return this.components.has(name);
86
+ }
87
+ };
88
+ var componentRegistry = new ComponentRegistry();
89
+ async function analyzeCodeWithAST(code) {
90
+ await componentRegistry.load();
91
+ const violations = [];
92
+ const { descriptor, errors } = parse(code);
93
+ if (errors.length > 0) {
94
+ return violations;
95
+ }
96
+ const scriptContent = descriptor.scriptSetup?.content || descriptor.script?.content || "";
97
+ if (scriptContent) {
98
+ const sourceFile = ts.createSourceFile(
99
+ "temp.ts",
100
+ scriptContent,
101
+ ts.ScriptTarget.Latest,
102
+ true
103
+ );
104
+ ts.forEachChild(sourceFile, (node) => {
105
+ if (ts.isImportDeclaration(node)) {
106
+ checkImport(node, violations, sourceFile);
107
+ }
108
+ });
109
+ }
110
+ if (descriptor.template?.ast) {
111
+ checkTemplate(descriptor.template.ast, violations, null);
112
+ }
113
+ if (/<Dropdown[\s\S]*?<template\s+(#menu|v-slot:menu)>/i.test(code)) {
114
+ violations.push({
115
+ rule: "DropdownMenu \u4E0D\u80FD\u653E\u5728 #menu \u63D2\u69FD\u4E2D",
116
+ match: "<template #menu>...<DropdownMenu>",
117
+ suggestion: "\u8BF7\u79FB\u9664 <template #menu>\uFF0C\u5C06 DropdownMenu \u76F4\u63A5\u4F5C\u4E3A Dropdown \u7684\u7B2C\u4E8C\u4E2A\u5B50\u5143\u7D20"
118
+ });
119
+ }
120
+ if (scriptContent && descriptor.template?.content) {
121
+ const scriptBindings = extractScriptBindings(scriptContent);
122
+ checkTemplateVariables(descriptor.template.content, scriptBindings, violations);
123
+ }
124
+ return violations;
125
+ }
126
+ function extractScriptBindings(scriptContent) {
127
+ const bindings = /* @__PURE__ */ new Set();
128
+ const vueBuiltins = [
129
+ "ref",
130
+ "reactive",
131
+ "computed",
132
+ "watch",
133
+ "watchEffect",
134
+ "onMounted",
135
+ "onUnmounted",
136
+ "onBeforeMount",
137
+ "onBeforeUnmount",
138
+ "defineProps",
139
+ "defineEmits",
140
+ "defineExpose",
141
+ "withDefaults",
142
+ "toRef",
143
+ "toRefs",
144
+ "nextTick",
145
+ "inject",
146
+ "provide",
147
+ "readonly",
148
+ "shallowRef",
149
+ "shallowReactive",
150
+ "triggerRef",
151
+ "customRef",
152
+ "h",
153
+ "createVNode",
154
+ "resolveComponent",
155
+ "getCurrentInstance"
156
+ ];
157
+ vueBuiltins.forEach((b) => bindings.add(b));
158
+ [
159
+ "console",
160
+ "window",
161
+ "document",
162
+ "Math",
163
+ "JSON",
164
+ "Date",
165
+ "Array",
166
+ "Object",
167
+ "String",
168
+ "Number",
169
+ "Boolean",
170
+ "Promise",
171
+ "Error",
172
+ "RegExp",
173
+ "Map",
174
+ "Set",
175
+ "parseInt",
176
+ "parseFloat",
177
+ "isNaN",
178
+ "isFinite",
179
+ "setTimeout",
180
+ "setInterval",
181
+ "clearTimeout",
182
+ "clearInterval",
183
+ "fetch",
184
+ "undefined",
185
+ "null",
186
+ "true",
187
+ "false"
188
+ ].forEach((b) => bindings.add(b));
189
+ try {
190
+ let visit2 = function(node) {
191
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
192
+ bindings.add(node.name.text);
193
+ }
194
+ if (ts.isFunctionDeclaration(node) && node.name) {
195
+ bindings.add(node.name.text);
196
+ }
197
+ if (ts.isImportDeclaration(node)) {
198
+ const namedBindings = node.importClause?.namedBindings;
199
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
200
+ namedBindings.elements.forEach((element) => {
201
+ bindings.add(element.name.text);
202
+ });
203
+ }
204
+ if (node.importClause?.name) {
205
+ bindings.add(node.importClause.name.text);
206
+ }
207
+ }
208
+ if (ts.isCallExpression(node)) {
209
+ const expr = node.expression;
210
+ if (ts.isIdentifier(expr) && expr.text === "defineProps") {
211
+ }
212
+ }
213
+ ts.forEachChild(node, visit2);
214
+ };
215
+ var visit = visit2;
216
+ const sourceFile = ts.createSourceFile(
217
+ "temp.ts",
218
+ scriptContent,
219
+ ts.ScriptTarget.Latest,
220
+ true
221
+ );
222
+ visit2(sourceFile);
223
+ } catch {
224
+ }
225
+ return bindings;
226
+ }
227
+ function checkTemplateVariables(templateContent, bindings, violations) {
228
+ const interpolationRegex = /\{\{\s*([^}]+)\s*\}\}/g;
229
+ let match;
230
+ while ((match = interpolationRegex.exec(templateContent)) !== null) {
231
+ const expression = match[1].trim();
232
+ const identifiers = extractIdentifiersFromExpression(expression);
233
+ for (const id of identifiers) {
234
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
235
+ violations.push({
236
+ rule: `\u6A21\u677F\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
237
+ match: `{{ ${expression} }}`,
238
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}\uFF0C\u6216\u68C0\u67E5\u62FC\u5199\u662F\u5426\u6B63\u786E`
239
+ });
240
+ }
241
+ }
242
+ }
243
+ const bindRegex = /(?::|v-bind:)[\w.-]+="([^"]+)"/g;
244
+ while ((match = bindRegex.exec(templateContent)) !== null) {
245
+ const expression = match[1].trim();
246
+ const identifiers = extractIdentifiersFromExpression(expression);
247
+ for (const id of identifiers) {
248
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
249
+ violations.push({
250
+ rule: `\u7ED1\u5B9A\u5C5E\u6027\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u53D8\u91CF: ${id}`,
251
+ match: match[0],
252
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u53D8\u91CF ${id}`
253
+ });
254
+ }
255
+ }
256
+ }
257
+ const eventRegex = /(?:@|v-on:)[\w.-]+="([^"]+)"/g;
258
+ while ((match = eventRegex.exec(templateContent)) !== null) {
259
+ const expression = match[1].trim();
260
+ if (expression.includes("(") || expression.includes("=")) continue;
261
+ const identifiers = extractIdentifiersFromExpression(expression);
262
+ for (const id of identifiers) {
263
+ if (!bindings.has(id) && !isTemplateBuiltin(id)) {
264
+ violations.push({
265
+ rule: `\u4E8B\u4EF6\u5904\u7406\u5668\u5F15\u7528\u4E86\u672A\u5B9A\u4E49\u7684\u51FD\u6570: ${id}`,
266
+ match: match[0],
267
+ suggestion: `\u8BF7\u786E\u4FDD\u5728 <script setup> \u4E2D\u5B9A\u4E49\u51FD\u6570 ${id}`
268
+ });
269
+ }
270
+ }
271
+ }
272
+ }
273
+ function extractIdentifiersFromExpression(expression) {
274
+ expression = expression.replace(/'[^']*'/g, "").replace(/"[^"]*"/g, "").replace(/`[^`]*`/g, "");
275
+ const identifiers = [];
276
+ const idRegex = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
277
+ let match;
278
+ while ((match = idRegex.exec(expression)) !== null) {
279
+ const id = match[1];
280
+ if (!isJsKeyword(id)) {
281
+ identifiers.push(id);
282
+ }
283
+ }
284
+ return identifiers;
285
+ }
286
+ function isTemplateBuiltin(id) {
287
+ const builtins = [
288
+ "$event",
289
+ "$refs",
290
+ "$slots",
291
+ "$attrs",
292
+ "$emit",
293
+ "$route",
294
+ "$router",
295
+ "$t",
296
+ "$i18n",
297
+ "item",
298
+ "index",
299
+ "key",
300
+ "value"
301
+ ];
302
+ return builtins.includes(id);
303
+ }
304
+ function isJsKeyword(id) {
305
+ const keywords = [
306
+ "if",
307
+ "else",
308
+ "for",
309
+ "while",
310
+ "do",
311
+ "switch",
312
+ "case",
313
+ "break",
314
+ "continue",
315
+ "return",
316
+ "try",
317
+ "catch",
318
+ "finally",
319
+ "throw",
320
+ "new",
321
+ "delete",
322
+ "typeof",
323
+ "instanceof",
324
+ "void",
325
+ "in",
326
+ "of",
327
+ "this",
328
+ "class",
329
+ "extends",
330
+ "super",
331
+ "import",
332
+ "export",
333
+ "default",
334
+ "const",
335
+ "let",
336
+ "var",
337
+ "function",
338
+ "async",
339
+ "await",
340
+ "yield",
341
+ "true",
342
+ "false",
343
+ "null",
344
+ "undefined",
345
+ "NaN",
346
+ "Infinity"
347
+ ];
348
+ return keywords.includes(id);
349
+ }
350
+ function checkImport(node, violations, sourceFile) {
351
+ const moduleSpecifier = node.moduleSpecifier.getText(sourceFile).replace(/['"]/g, "");
352
+ if (moduleSpecifier === "@king-design/vue" || moduleSpecifier === "@ksyun-internal/versatile") {
353
+ const namedBindings = node.importClause?.namedBindings;
354
+ if (namedBindings && ts.isNamedImports(namedBindings)) {
355
+ namedBindings.elements.forEach((element) => {
356
+ const originalName = element.propertyName ? element.propertyName.text : element.name.text;
357
+ const localName = element.name.text;
358
+ const isAlias = !!element.propertyName;
359
+ if (isAlias) {
360
+ violations.push({
361
+ rule: `\u7981\u6B62\u5BF9\u7EC4\u4EF6 ${originalName} \u4F7F\u7528\u522B\u540D\u5BFC\u5165`,
362
+ match: `${originalName} as ${localName}`,
363
+ suggestion: `\u76F4\u63A5\u4F7F\u7528\u539F\u540D: import { ${originalName} } from '${moduleSpecifier}'`
364
+ });
365
+ }
366
+ if (componentRegistry.isKnownComponent(originalName)) {
367
+ const metadata = componentRegistry.getComponent(originalName);
368
+ const expectedPackageMatch = metadata?.importStatement?.match(/from\s+['"]([^'"]+)['"]/);
369
+ if (expectedPackageMatch) {
370
+ const expectedPackage = expectedPackageMatch[1];
371
+ if (expectedPackage !== moduleSpecifier) {
372
+ violations.push({
373
+ rule: `\u7EC4\u4EF6 ${originalName} \u5BFC\u5165\u6E90\u9519\u8BEF`,
374
+ match: `import { ... } from '${moduleSpecifier}'`,
375
+ suggestion: `\u5E94\u4ECE '${expectedPackage}' \u5BFC\u5165`
376
+ });
377
+ }
378
+ }
379
+ } else {
380
+ violations.push({
381
+ rule: "\u5F15\u7528\u4E86\u4E0D\u5B58\u5728\u7684\u7EC4\u4EF6/\u5BFC\u51FA",
382
+ match: originalName,
383
+ suggestion: `\u8BF7\u786E\u8BA4 ${originalName} \u662F\u5426\u5B58\u5728\u4E8E ${moduleSpecifier}\u3002\u5EFA\u8BAE\u67E5\u770B\u6587\u6863\u3002`
384
+ });
385
+ }
386
+ });
387
+ }
388
+ }
389
+ }
390
+ var NESTING_RULES = {
391
+ "DropdownItem": ["DropdownMenu"],
392
+ "DropdownMenu": ["Dropdown"],
393
+ "TableColumn": ["Table"],
394
+ "FormItem": ["Form"],
395
+ "Tab": ["Tabs"],
396
+ "Step": ["Steps"],
397
+ "MenuItem": ["Menu", "SubMenu"],
398
+ "SubMenu": ["Menu"],
399
+ "BreadcrumbItem": ["Breadcrumb"],
400
+ "CollapseItem": ["Collapse"],
401
+ "CarouselItem": ["Carousel"],
402
+ "DescriptionItem": ["Descriptions"]
403
+ };
404
+ function checkTemplate(node, violations, parentTagName) {
405
+ if (node.type === 1) {
406
+ const tagName = node.tag;
407
+ if (componentRegistry.isKnownComponent(tagName)) {
408
+ const metadata = componentRegistry.getComponent(tagName);
409
+ const requiredParents = metadata?.requiredParent ? [metadata.requiredParent] : NESTING_RULES[tagName];
410
+ if (requiredParents && requiredParents.length > 0) {
411
+ if (parentTagName && !requiredParents.includes(parentTagName)) {
412
+ const isInSlotTemplate = parentTagName === "template";
413
+ if (!isInSlotTemplate) {
414
+ violations.push({
415
+ rule: `${tagName} \u5FC5\u987B\u653E\u5728 ${requiredParents.join(" \u6216 ")} \u7EC4\u4EF6\u5185`,
416
+ match: tagName,
417
+ suggestion: `\u8BF7\u5C06 ${tagName} \u79FB\u52A8\u5230 ${requiredParents[0]} \u7EC4\u4EF6\u4E2D`
418
+ });
419
+ }
420
+ }
421
+ }
422
+ }
423
+ if (tagName === "Table") {
424
+ node.props.forEach((prop) => {
425
+ if (prop.type === 6 && (prop.name === "rowKey" || prop.name === "row-key")) {
426
+ violations.push({
427
+ rule: "Table\u7EC4\u4EF6 rowKey \u5C5E\u6027 usage \u9519\u8BEF",
428
+ match: `rowKey="${prop.value?.content || ""}"`,
429
+ suggestion: 'rowKey \u5FC5\u987B\u662F\u4E00\u4E2A\u8FD4\u56DE\u552F\u4E00\u503C\u7684\u51FD\u6570\u3002\u8BF7\u4F7F\u7528\u7ED1\u5B9A\u8BED\u6CD5\uFF0C\u4F8B\u5982 :rowKey="(row) => row.id"'
430
+ });
431
+ }
432
+ });
433
+ }
434
+ if (tagName === "Icon") {
435
+ node.props.forEach((prop) => {
436
+ if (prop.type === 6 && prop.name === "name") {
437
+ violations.push({
438
+ rule: "Icon \u7EC4\u4EF6\u7981\u6B62\u4F7F\u7528 name \u5C5E\u6027",
439
+ match: `name="${prop.value?.content || ""}"`,
440
+ suggestion: '\u8BF7\u4F7F\u7528 class \u5C5E\u6027\u6307\u5B9A\u56FE\u6807\uFF0C\u4F8B\u5982: class="k-icon-search"'
441
+ });
442
+ }
443
+ });
444
+ }
445
+ if (componentRegistry.isKnownComponent(tagName)) {
446
+ const metadata = componentRegistry.getComponent(tagName);
447
+ if (metadata) {
448
+ let customTagName = null;
449
+ const tagNameProp = node.props.find(
450
+ (p) => p.type === 6 && p.name === "tagName" || p.type === 7 && p.name === "bind" && p.arg?.content === "tagName"
451
+ );
452
+ if (tagNameProp?.type === 6 && tagNameProp.value?.content) {
453
+ customTagName = tagNameProp.value.content;
454
+ }
455
+ const dynamicAllowedProps = [];
456
+ if (customTagName === "a") {
457
+ dynamicAllowedProps.push("href", "target", "rel", "download");
458
+ }
459
+ node.props.forEach((prop) => {
460
+ if (prop.type === 6) {
461
+ const propName = prop.name;
462
+ const camelCaseProp = propName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
463
+ const isPropValid = metadata.props.some(
464
+ (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
465
+ );
466
+ const isDynamicallyAllowed = dynamicAllowedProps.includes(propName);
467
+ if (!isPropValid && !isDynamicallyAllowed && !["class", "style", "key", "ref"].includes(propName) && !propName.startsWith("data-")) {
468
+ if (metadata.props.length > 0) {
469
+ violations.push({
470
+ rule: `\u5C5E\u6027 ${propName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
471
+ match: propName,
472
+ suggestion: `\u53EF\u7528\u5C5E\u6027: ${metadata.props.map((p) => p.name).join(", ")}`
473
+ });
474
+ }
475
+ }
476
+ if (isPropValid) {
477
+ const targetProp = metadata.props.find(
478
+ (p) => p.name === propName || p.name === camelCaseProp || p.name.toLowerCase() === propName.toLowerCase()
479
+ );
480
+ if (targetProp) {
481
+ const attrValue = prop.value?.content;
482
+ if (attrValue) {
483
+ let cleanAllowedValues = [];
484
+ if (targetProp.allowedValues && targetProp.allowedValues.length > 0) {
485
+ cleanAllowedValues = targetProp.allowedValues.map((av) => String(av.value).replace(/['"]/g, ""));
486
+ } else if (targetProp.type.kind === "union" && targetProp.type.unionTypes) {
487
+ cleanAllowedValues = targetProp.type.unionTypes.map((v) => String(v).replace(/['"]/g, ""));
488
+ }
489
+ if (cleanAllowedValues.length > 0) {
490
+ const isValueValid = cleanAllowedValues.includes(attrValue);
491
+ if (!isValueValid) {
492
+ console.log(`\u274C [AST Reviewer] \u5C5E\u6027\u503C\u6821\u9A8C\u5931\u8D25: ${tagName}.${propName}="${attrValue}" (\u5141\u8BB8\u503C: ${cleanAllowedValues.join(" | ")})`);
493
+ violations.push({
494
+ rule: `${tagName} \u7684\u5C5E\u6027 ${propName} \u7684\u503C "${attrValue}" \u65E0\u6548`,
495
+ match: `${propName}="${attrValue}"`,
496
+ suggestion: `\u5141\u8BB8\u7684\u503C: ${cleanAllowedValues.join(" | ")}`
497
+ });
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+ } else if (prop.type === 7) {
504
+ const dirName = prop.name;
505
+ const arg = prop.arg?.content;
506
+ if (dirName === "on" && arg) {
507
+ const eventName = arg;
508
+ if (eventName.startsWith("$change:") || eventName.startsWith("update:")) {
509
+ return;
510
+ }
511
+ const camelCaseEvent = eventName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
512
+ const isEventValid = metadata.events.some(
513
+ (e) => e.name === eventName || e.name === camelCaseEvent || e.name.toLowerCase() === eventName.toLowerCase()
514
+ );
515
+ const nativeEvents = [
516
+ "click",
517
+ "dblclick",
518
+ "mousedown",
519
+ "mouseup",
520
+ "mousemove",
521
+ "mouseenter",
522
+ "mouseleave",
523
+ "keydown",
524
+ "keyup",
525
+ "keypress",
526
+ "focus",
527
+ "blur",
528
+ "input",
529
+ "submit",
530
+ "scroll",
531
+ "resize",
532
+ "contextmenu",
533
+ "wheel",
534
+ "touchstart",
535
+ "touchend",
536
+ "touchmove"
537
+ ];
538
+ const isNativeEvent = nativeEvents.includes(eventName);
539
+ if (!isEventValid && !isNativeEvent && metadata.events.length > 0) {
540
+ violations.push({
541
+ rule: `\u4E8B\u4EF6 @${eventName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
542
+ match: `@${eventName}`,
543
+ suggestion: `\u53EF\u7528\u4E8B\u4EF6: ${metadata.events.map((e) => e.name).join(", ") || "\u65E0\u81EA\u5B9A\u4E49\u4E8B\u4EF6"}`
544
+ });
545
+ }
546
+ }
547
+ }
548
+ });
549
+ if (node.children && metadata.slots && metadata.slots.length > 0) {
550
+ node.children.forEach((child) => {
551
+ if (child.type === 1 && child.tag === "template") {
552
+ child.props.forEach((prop) => {
553
+ if (prop.type === 7 && prop.name === "slot") {
554
+ const slotName = prop.arg?.content || "default";
555
+ const isSlotValid = metadata.slots.some((s) => s.name === slotName);
556
+ if (!isSlotValid) {
557
+ violations.push({
558
+ rule: `\u63D2\u69FD #${slotName} \u4E0D\u5B58\u5728\u4E8E ${tagName}`,
559
+ match: `#${slotName}`,
560
+ suggestion: `\u53EF\u7528\u63D2\u69FD: ${metadata.slots.map((s) => s.name).join(", ")}`
561
+ });
562
+ }
563
+ }
564
+ });
565
+ }
566
+ });
567
+ }
568
+ }
569
+ }
570
+ }
571
+ if (node.children) {
572
+ const currentTag = node.type === 1 ? node.tag : null;
573
+ node.children.forEach((child) => checkTemplate(child, violations, currentTag));
574
+ }
575
+ }
576
+
577
+ export { analyzeCodeWithAST, componentRegistry };
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ var sucrase = require('sucrase');
4
+
5
+ // src/tools/sfcCompiler.ts
6
+ function scopeStyles(css, scopeClass) {
7
+ return css.replace(/([^\r\n,{}]+)(,(?=[^}]*{)|\s*{)/g, (match, selector, suffix) => {
8
+ if (selector.trim().startsWith("@")) return match;
9
+ const scopedSelector = selector.split(",").map((s) => {
10
+ s = s.trim();
11
+ if (s === "body" || s === "html") return `.${scopeClass}`;
12
+ return `.${scopeClass} ${s}`;
13
+ }).join(", ");
14
+ return `${scopedSelector}${suffix}`;
15
+ });
16
+ }
17
+ function compileSFC(code, scopeId) {
18
+ try {
19
+ const templateMatch = code.match(/<template>([\s\S]*)<\/template>/);
20
+ const scriptMatch = code.match(/<script[\s\S]*?>([\s\S]*)<\/script>/);
21
+ const styleMatch = code.match(/<style[\s\S]*?>([\s\S]*)<\/style>/);
22
+ const template = templateMatch && templateMatch[1] || "";
23
+ const script = scriptMatch && scriptMatch[1] || "";
24
+ const styleRaw = styleMatch && styleMatch[1] || "";
25
+ const style = styleRaw ? scopeStyles(styleRaw, scopeId) : "";
26
+ if (!template && !script) {
27
+ return { template: "", script: "", style: "", bindings: [] };
28
+ }
29
+ let cleanScript = script;
30
+ cleanScript = cleanScript.replace(/import\s+[\s\S]*?from\s+['"].*?['"];?/g, "");
31
+ cleanScript = cleanScript.replace(/import\s+type\s+[\s\S]*?from\s+['"].*?['"];?/g, "");
32
+ const result = sucrase.transform(cleanScript, {
33
+ transforms: ["typescript"],
34
+ disableESTransforms: true
35
+ });
36
+ if (result && typeof result.code === "string") {
37
+ cleanScript = result.code;
38
+ }
39
+ const varNames = [];
40
+ const lines = cleanScript.split("\n");
41
+ let baseIndent = -1;
42
+ for (const line of lines) {
43
+ if (line.trim()) {
44
+ const match = line.match(/^(\s*)/);
45
+ if (match && match[1] !== void 0) {
46
+ baseIndent = match[1].length;
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ if (baseIndent === -1) baseIndent = 0;
52
+ for (const line of lines) {
53
+ const lineIndentMatch = line.match(/^(\s*)/);
54
+ const lineIndent = lineIndentMatch && typeof lineIndentMatch[1] === "string" ? lineIndentMatch[1].length : 0;
55
+ if (!line.trim() || line.trim().startsWith("//")) continue;
56
+ if (lineIndent === baseIndent) {
57
+ const trimmed = line.trim();
58
+ const stdMatch = trimmed.match(/^(?:const|let|var|function)\s+(\w+)/);
59
+ if (stdMatch && stdMatch[1]) {
60
+ varNames.push(stdMatch[1]);
61
+ continue;
62
+ }
63
+ const objMatch = trimmed.match(/^(?:const|let|var)\s+\{([^}]+)\}\s*=/);
64
+ if (objMatch && typeof objMatch[1] === "string") {
65
+ const content = objMatch[1];
66
+ const vars = content.split(",").map((v) => (v.split(":")[0] || "").trim());
67
+ vars.forEach((v) => {
68
+ const cleanName = v.replace(/\s+as\s+\w+/, "").trim();
69
+ if (/^[a-zA-Z_$][\w$]*$/.test(cleanName)) varNames.push(cleanName);
70
+ });
71
+ continue;
72
+ }
73
+ const arrMatch = trimmed.match(/^(?:const|let|var)\s+\[([^\]]+)\]\s*=/);
74
+ if (arrMatch && typeof arrMatch[1] === "string") {
75
+ const content = arrMatch[1];
76
+ const vars = content.split(",").map((v) => v.trim());
77
+ vars.forEach((v) => {
78
+ if (v && /^[a-zA-Z_$][\w$]*$/.test(v)) varNames.push(v);
79
+ });
80
+ continue;
81
+ }
82
+ }
83
+ }
84
+ return {
85
+ template,
86
+ script: cleanScript,
87
+ style,
88
+ bindings: [...new Set(varNames)]
89
+ };
90
+ } catch (e) {
91
+ return {
92
+ template: "",
93
+ script: "",
94
+ style: "",
95
+ bindings: [],
96
+ error: e.message
97
+ };
98
+ }
99
+ }
100
+
101
+ exports.compileSFC = compileSFC;
102
+ exports.scopeStyles = scopeStyles;