frontend-guardian-core 3.1.0 → 3.5.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/bin/fg-core.js +97 -7
- package/bin/fg-lsp.js +67 -0
- package/dist/engine/cache.d.ts +10 -3
- package/dist/engine/cache.d.ts.map +1 -1
- package/dist/engine/cache.js +30 -4
- package/dist/engine/cache.js.map +1 -1
- package/dist/engine/rule-engine.d.ts +30 -0
- package/dist/engine/rule-engine.d.ts.map +1 -1
- package/dist/engine/rule-engine.js +159 -3
- package/dist/engine/rule-engine.js.map +1 -1
- package/dist/ide/incremental-diagnostic.d.ts +84 -0
- package/dist/ide/incremental-diagnostic.d.ts.map +1 -0
- package/dist/ide/incremental-diagnostic.js +136 -0
- package/dist/ide/incremental-diagnostic.js.map +1 -0
- package/dist/ide/lsp-server.d.ts +23 -0
- package/dist/ide/lsp-server.d.ts.map +1 -0
- package/dist/ide/lsp-server.js +187 -0
- package/dist/ide/lsp-server.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -2
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/baseline.d.ts +31 -3
- package/dist/utils/baseline.d.ts.map +1 -1
- package/dist/utils/baseline.js +125 -5
- package/dist/utils/baseline.js.map +1 -1
- package/dist/utils/codeowners.d.ts +31 -0
- package/dist/utils/codeowners.d.ts.map +1 -0
- package/dist/utils/codeowners.js +97 -0
- package/dist/utils/codeowners.js.map +1 -0
- package/dist/utils/concurrent.d.ts +18 -0
- package/dist/utils/concurrent.d.ts.map +1 -1
- package/dist/utils/concurrent.js +59 -0
- package/dist/utils/concurrent.js.map +1 -1
- package/dist/utils/notification.d.ts +52 -0
- package/dist/utils/notification.d.ts.map +1 -0
- package/dist/utils/notification.js +226 -0
- package/dist/utils/notification.js.map +1 -0
- package/package.json +5 -2
package/bin/fg-core.js
CHANGED
|
@@ -49,6 +49,10 @@ import {
|
|
|
49
49
|
compareHistoryReports,
|
|
50
50
|
formatHistoryCompare,
|
|
51
51
|
formatHistoryCompareJson,
|
|
52
|
+
CodeownersParser,
|
|
53
|
+
sendNotifications,
|
|
54
|
+
detectNotificationConfig,
|
|
55
|
+
buildNotificationPayload,
|
|
52
56
|
} from "../dist/index.js";
|
|
53
57
|
import pc from "picocolors";
|
|
54
58
|
import { runWatchMode } from "./watch-mode.js";
|
|
@@ -81,12 +85,13 @@ const MODULE_RULES = {
|
|
|
81
85
|
|
|
82
86
|
function showHelp() {
|
|
83
87
|
console.log(`
|
|
84
|
-
Frontend Guardian Core v3.
|
|
88
|
+
Frontend Guardian Core v3.5.0
|
|
85
89
|
|
|
86
90
|
Usage:
|
|
87
91
|
fg-core <project-dir> [options]
|
|
88
92
|
|
|
89
93
|
Options:
|
|
94
|
+
--scan 全量扫描(等价于 --module all)
|
|
90
95
|
--module <name> 扫描模块: i18n | performance | a11y | security | naming | cross-file | component | hooks | platform | svelte | all
|
|
91
96
|
--severity <level> 最低严重级别: critical | warning | suggestion (默认: suggestion)
|
|
92
97
|
--files <pattern> 仅扫描匹配的文件
|
|
@@ -131,6 +136,9 @@ Options:
|
|
|
131
136
|
--no-cross-deps 禁用跨包依赖分析(配合 --monorepo)
|
|
132
137
|
--ai-fix 启用 AI 修复建议(需配置 FG_AI_API_KEY 环境变量)
|
|
133
138
|
--ai-model <model> 指定 AI 模型(如 gpt-4o-mini / claude-3-5-sonnet,配合 --ai-fix)
|
|
139
|
+
--team-baseline <url> 团队共享 baseline URL(支持远程加载,1小时缓存)
|
|
140
|
+
--notify 扫描完成后发送 webhook 通知(需配置 notifications)
|
|
141
|
+
--assign 通过 CODEOWNERS 为 issue 推断责任人
|
|
134
142
|
--help, -h 显示帮助
|
|
135
143
|
|
|
136
144
|
Examples:
|
|
@@ -198,10 +206,16 @@ async function main() {
|
|
|
198
206
|
crossDeps: true,
|
|
199
207
|
aiFix: false,
|
|
200
208
|
aiModel: undefined,
|
|
209
|
+
teamBaseline: undefined,
|
|
210
|
+
notify: false,
|
|
211
|
+
assign: false,
|
|
201
212
|
};
|
|
202
213
|
|
|
203
214
|
for (let i = 0; i < args.length; i++) {
|
|
204
215
|
switch (args[i]) {
|
|
216
|
+
case "--scan":
|
|
217
|
+
options.module = "all";
|
|
218
|
+
break;
|
|
205
219
|
case "--module":
|
|
206
220
|
options.module = args[++i];
|
|
207
221
|
break;
|
|
@@ -341,6 +355,15 @@ async function main() {
|
|
|
341
355
|
case "--ai-model":
|
|
342
356
|
options.aiModel = args[++i];
|
|
343
357
|
break;
|
|
358
|
+
case "--team-baseline":
|
|
359
|
+
options.teamBaseline = args[++i];
|
|
360
|
+
break;
|
|
361
|
+
case "--notify":
|
|
362
|
+
options.notify = true;
|
|
363
|
+
break;
|
|
364
|
+
case "--assign":
|
|
365
|
+
options.assign = true;
|
|
366
|
+
break;
|
|
344
367
|
case "--help":
|
|
345
368
|
case "-h":
|
|
346
369
|
showHelp();
|
|
@@ -750,12 +773,18 @@ async function runAllModules(options, cacheInstance) {
|
|
|
750
773
|
}
|
|
751
774
|
}
|
|
752
775
|
|
|
753
|
-
//
|
|
776
|
+
// v3.5.0: 为 issues 添加责任人(CODEOWNERS)
|
|
777
|
+
if (options.assign) {
|
|
778
|
+
applyAssignees(allResults, externalResults, options.projectDir);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// v2.3.0 / v3.5.0: Baseline 模式 —— 生成或应用 baseline 过滤(支持远程团队 baseline)
|
|
754
782
|
let baselineResult = null;
|
|
755
|
-
|
|
783
|
+
const baselinePath = options.teamBaseline || options.baseline;
|
|
784
|
+
if (baselinePath) {
|
|
756
785
|
const allIssues = collectAllIssues(allResults, externalResults);
|
|
757
786
|
if (options.generateBaseline) {
|
|
758
|
-
const baseline = new BaselineManager(
|
|
787
|
+
const baseline = new BaselineManager(baselinePath, options.projectDir);
|
|
759
788
|
baseline.save(allIssues, { projectDir: options.projectDir });
|
|
760
789
|
if (!options.json) {
|
|
761
790
|
console.log(pc.cyan(`📋 Baseline 已生成: ${baseline.getPath()}`));
|
|
@@ -763,7 +792,7 @@ async function runAllModules(options, cacheInstance) {
|
|
|
763
792
|
}
|
|
764
793
|
process.exit(0);
|
|
765
794
|
}
|
|
766
|
-
baselineResult = applyBaselineToResults(allResults, externalResults,
|
|
795
|
+
baselineResult = await applyBaselineToResults(allResults, externalResults, baselinePath, options.projectDir);
|
|
767
796
|
if (baselineResult.baselineLoaded && !options.json) {
|
|
768
797
|
console.log(
|
|
769
798
|
pc.cyan(
|
|
@@ -1066,6 +1095,31 @@ async function runAllModules(options, cacheInstance) {
|
|
|
1066
1095
|
}
|
|
1067
1096
|
}
|
|
1068
1097
|
|
|
1098
|
+
// v3.5.0: 发送扫描结果通知
|
|
1099
|
+
if (options.notify) {
|
|
1100
|
+
const notifyConfig = detectNotificationConfig();
|
|
1101
|
+
if (
|
|
1102
|
+
notifyConfig.feishu?.enabled ||
|
|
1103
|
+
notifyConfig.dingtalk?.enabled ||
|
|
1104
|
+
notifyConfig.wecom?.enabled ||
|
|
1105
|
+
notifyConfig.slack?.enabled
|
|
1106
|
+
) {
|
|
1107
|
+
const moduleResults = MODULES.map((m) => allResults[m]).filter(Boolean);
|
|
1108
|
+
const payload = buildNotificationPayload(moduleResults, {
|
|
1109
|
+
project: options.projectDir,
|
|
1110
|
+
duration: totalDuration,
|
|
1111
|
+
gatePassed: totalCritical === 0,
|
|
1112
|
+
});
|
|
1113
|
+
const notifyResults = await sendNotifications(payload, notifyConfig);
|
|
1114
|
+
for (const nr of notifyResults) {
|
|
1115
|
+
const icon = nr.success ? pc.green("✅") : pc.red("❌");
|
|
1116
|
+
console.log(pc.gray(` ${icon} ${nr.channel}: ${nr.success ? "已发送" : nr.error}`));
|
|
1117
|
+
}
|
|
1118
|
+
} else {
|
|
1119
|
+
console.log(pc.yellow("⚠️ 未配置通知渠道。请设置 FG_NOTIFY_FEISHU / FG_NOTIFY_DINGTALK / FG_NOTIFY_WECOM / FG_NOTIFY_SLACK 环境变量"));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1069
1123
|
console.log(
|
|
1070
1124
|
pc.gray(
|
|
1071
1125
|
`⏱️ 总耗时: ${totalDuration}ms | 扫描 ${totalFilesScanned} 个文件 | ${totalFilesWithIssues} 个文件有问题`
|
|
@@ -1478,9 +1532,17 @@ function collectAllIssues(allResults, externalResults) {
|
|
|
1478
1532
|
}
|
|
1479
1533
|
|
|
1480
1534
|
/** 应用 baseline 过滤到 allResults,返回 BaselineResult */
|
|
1481
|
-
function applyBaselineToResults(allResults, externalResults, baselinePath, projectDir) {
|
|
1535
|
+
async function applyBaselineToResults(allResults, externalResults, baselinePath, projectDir) {
|
|
1482
1536
|
const allIssues = collectAllIssues(allResults, externalResults);
|
|
1483
|
-
const
|
|
1537
|
+
const isRemote = baselinePath.startsWith("http://") || baselinePath.startsWith("https://");
|
|
1538
|
+
const baseline = new BaselineManager(baselinePath, projectDir, {
|
|
1539
|
+
teamBaselineUrl: isRemote ? baselinePath : undefined,
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
if (isRemote) {
|
|
1543
|
+
await baseline.init();
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1484
1546
|
const baselineResult = baseline.filterNewIssues(allIssues);
|
|
1485
1547
|
|
|
1486
1548
|
if (!baselineResult.baselineLoaded) {
|
|
@@ -1519,6 +1581,34 @@ function applyBaselineToResults(allResults, externalResults, baselinePath, proje
|
|
|
1519
1581
|
return baselineResult;
|
|
1520
1582
|
}
|
|
1521
1583
|
|
|
1584
|
+
/** 为所有 issues 添加责任人(CODEOWNERS) */
|
|
1585
|
+
function applyAssignees(allResults, externalResults, projectDir) {
|
|
1586
|
+
const parser = new CodeownersParser(projectDir);
|
|
1587
|
+
if (!parser.hasCodeowners()) return;
|
|
1588
|
+
|
|
1589
|
+
for (const mod of MODULES) {
|
|
1590
|
+
const r = allResults[mod];
|
|
1591
|
+
if (!r) continue;
|
|
1592
|
+
for (const sev of ["critical", "warning", "suggestion"]) {
|
|
1593
|
+
for (const issue of r.issues[sev]) {
|
|
1594
|
+
const owner = parser.getOwner(issue.file);
|
|
1595
|
+
if (owner) {
|
|
1596
|
+
issue.assignee = owner;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
for (const er of externalResults) {
|
|
1603
|
+
for (const issue of er.issues) {
|
|
1604
|
+
const owner = parser.getOwner(issue.file);
|
|
1605
|
+
if (owner) {
|
|
1606
|
+
issue.assignee = owner;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1522
1612
|
/** 重新计算 totals */
|
|
1523
1613
|
function recalculateTotals(allResults, externalResults) {
|
|
1524
1614
|
let critical = 0;
|
package/bin/fg-lsp.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Frontend Guardian LSP Server
|
|
4
|
+
* Usage: fg-lsp --stdio [--project-dir <dir>] [--config <file>]
|
|
5
|
+
*
|
|
6
|
+
* v3.3.0: Language Server Protocol 实现,为 IDE 提供实时诊断和快速修复。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { runLSPServer } from "../dist/ide/lsp-server.js";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
// 解析参数
|
|
16
|
+
let projectDir = process.cwd();
|
|
17
|
+
let configFile;
|
|
18
|
+
let minSeverity = "suggestion";
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < args.length; i++) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
if (arg === "--project-dir" || arg === "-p") {
|
|
23
|
+
projectDir = resolve(args[++i] ?? process.cwd());
|
|
24
|
+
} else if (arg === "--config" || arg === "-c") {
|
|
25
|
+
configFile = resolve(args[++i] ?? "");
|
|
26
|
+
} else if (arg === "--severity" || arg === "-s") {
|
|
27
|
+
minSeverity = args[++i] ?? "suggestion";
|
|
28
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
29
|
+
console.log(`Frontend Guardian LSP Server v3.3.0
|
|
30
|
+
|
|
31
|
+
Usage: fg-lsp [options]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
--project-dir, -p <dir> 项目根目录(默认当前目录)
|
|
35
|
+
--config, -c <file> 配置文件路径
|
|
36
|
+
--severity, -s <level> 最低严重级别: critical|warning|suggestion(默认 suggestion)
|
|
37
|
+
--help, -h 显示帮助
|
|
38
|
+
|
|
39
|
+
Environment:
|
|
40
|
+
FG_PROJECT_DIR 项目根目录(优先级低于 --project-dir)
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
fg-lsp --stdio
|
|
44
|
+
fg-lsp --project-dir ./my-project --config .frontend-guardian.yml
|
|
45
|
+
`);
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 环境变量回退
|
|
51
|
+
if (process.env.FG_PROJECT_DIR && !args.includes("--project-dir") && !args.includes("-p")) {
|
|
52
|
+
projectDir = resolve(process.env.FG_PROJECT_DIR);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 验证项目目录
|
|
56
|
+
if (!existsSync(projectDir)) {
|
|
57
|
+
console.error(`Error: Project directory does not exist: ${projectDir}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 启动 LSP 服务器
|
|
62
|
+
runLSPServer({
|
|
63
|
+
projectDir,
|
|
64
|
+
configFile,
|
|
65
|
+
minSeverity,
|
|
66
|
+
debounceMs: 300,
|
|
67
|
+
});
|
package/dist/engine/cache.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 2. 缓存持久化到 .frontend-guardian/cache.json
|
|
7
7
|
* 3. 自动过期策略(默认 7 天)
|
|
8
8
|
* 4. 兼容 --staged / --diff 增量模式
|
|
9
|
+
* 5. v3.2.0: AST 内存缓存 LRU 淘汰策略,防止大项目 OOM
|
|
9
10
|
*/
|
|
10
11
|
import type { Issue } from "../types.js";
|
|
11
12
|
export interface CacheEntry {
|
|
@@ -35,7 +36,9 @@ export declare class SmartCache {
|
|
|
35
36
|
private ttl;
|
|
36
37
|
/** v2.1.0: 内存级 AST 缓存(无需持久化,进程内复用) */
|
|
37
38
|
private astCache;
|
|
38
|
-
|
|
39
|
+
/** v3.2.0: AST 缓存最大条目数 */
|
|
40
|
+
private maxAstCacheSize;
|
|
41
|
+
constructor(projectDir: string, ttl?: number, maxAstCacheSize?: number);
|
|
39
42
|
/** 计算文件内容哈希 */
|
|
40
43
|
static computeHash(content: string): string;
|
|
41
44
|
/** 检查文件是否已缓存且未过期 */
|
|
@@ -56,10 +59,14 @@ export declare class SmartCache {
|
|
|
56
59
|
valid: number;
|
|
57
60
|
expired: number;
|
|
58
61
|
};
|
|
59
|
-
/** 获取缓存的 AST
|
|
62
|
+
/** 获取缓存的 AST(内存级,不持久化)— v3.2.0 增加 LRU 淘汰 */
|
|
60
63
|
getAst(filePath: string, content: string): unknown | undefined;
|
|
61
|
-
/** 缓存 AST 解析结果 */
|
|
64
|
+
/** 缓存 AST 解析结果 — v3.2.0 增加 LRU 淘汰 */
|
|
62
65
|
setAst(filePath: string, content: string, ast: unknown): void;
|
|
66
|
+
/** v3.2.0: 获取 AST 缓存当前大小 */
|
|
67
|
+
getAstCacheSize(): number;
|
|
68
|
+
/** v3.2.0: 获取 AST 缓存上限 */
|
|
69
|
+
getAstCacheLimit(): number;
|
|
63
70
|
/** 清理过期缓存 */
|
|
64
71
|
gc(): number;
|
|
65
72
|
/** 加载缓存 manifest */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/engine/cache.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/engine/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,MAAM,WAAW,UAAU;IACvB,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW;IACX,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,YAAY;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,WAAW,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC1B,WAAW;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACpC,eAAe;IACf,YAAY,EAAE,MAAM,CAAC;CACxB;AASD,qBAAa,UAAU;IACnB,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,GAAG,CAAS;IACpB,sCAAsC;IACtC,OAAO,CAAC,QAAQ,CAAqD;IACrE,0BAA0B;IAC1B,OAAO,CAAC,eAAe,CAAS;gBAEpB,UAAU,EAAE,MAAM,EAAE,GAAG,GAAE,MAAoB,EAAE,eAAe,CAAC,EAAE,MAAM;IAQnF,eAAe;IACf,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAI3C,oBAAoB;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO;IAkBpD,kBAAkB;IAClB,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,SAAS;IAK1C,eAAe;IACf,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI;IAS7D,aAAa;IACb,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAIlC,uBAAuB;IACvB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IASxC,cAAc;IACd,IAAI,IAAI,IAAI;IAWZ,aAAa;IACb,QAAQ,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE;IAkB7D,4CAA4C;IAC5C,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS;IAiB9D,qCAAqC;IACrC,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,IAAI;IAiB7D,4BAA4B;IAC5B,eAAe,IAAI,MAAM;IAIzB,0BAA0B;IAC1B,gBAAgB,IAAI,MAAM;IAI1B,aAAa;IACb,EAAE,IAAI,MAAM;IAcZ,oBAAoB;IACpB,OAAO,CAAC,YAAY;CAoBvB"}
|
package/dist/engine/cache.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* 2. 缓存持久化到 .frontend-guardian/cache.json
|
|
8
8
|
* 3. 自动过期策略(默认 7 天)
|
|
9
9
|
* 4. 兼容 --staged / --diff 增量模式
|
|
10
|
+
* 5. v3.2.0: AST 内存缓存 LRU 淘汰策略,防止大项目 OOM
|
|
10
11
|
*/
|
|
11
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
13
|
exports.SmartCache = void 0;
|
|
@@ -16,7 +17,9 @@ const node_path_1 = require("node:path");
|
|
|
16
17
|
/** 缓存默认存活时间(7 天) */
|
|
17
18
|
const DEFAULT_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
18
19
|
/** 引擎版本,规则变更时递增使缓存失效 */
|
|
19
|
-
const ENGINE_VERSION = "2.0
|
|
20
|
+
const ENGINE_VERSION = "3.2.0";
|
|
21
|
+
/** v3.2.0: AST 内存缓存默认最大条目数 */
|
|
22
|
+
const DEFAULT_AST_CACHE_SIZE = 200;
|
|
20
23
|
class SmartCache {
|
|
21
24
|
manifest;
|
|
22
25
|
cacheDir;
|
|
@@ -24,10 +27,13 @@ class SmartCache {
|
|
|
24
27
|
ttl;
|
|
25
28
|
/** v2.1.0: 内存级 AST 缓存(无需持久化,进程内复用) */
|
|
26
29
|
astCache = new Map();
|
|
27
|
-
|
|
30
|
+
/** v3.2.0: AST 缓存最大条目数 */
|
|
31
|
+
maxAstCacheSize;
|
|
32
|
+
constructor(projectDir, ttl = DEFAULT_TTL, maxAstCacheSize) {
|
|
28
33
|
this.cacheDir = (0, node_path_1.resolve)(projectDir, ".frontend-guardian");
|
|
29
34
|
this.cacheFile = (0, node_path_1.resolve)(this.cacheDir, "cache.json");
|
|
30
35
|
this.ttl = ttl;
|
|
36
|
+
this.maxAstCacheSize = maxAstCacheSize ?? DEFAULT_AST_CACHE_SIZE;
|
|
31
37
|
this.manifest = this.loadManifest();
|
|
32
38
|
}
|
|
33
39
|
/** 计算文件内容哈希 */
|
|
@@ -107,7 +113,7 @@ class SmartCache {
|
|
|
107
113
|
return { total: Object.keys(this.manifest.entries).length, valid, expired };
|
|
108
114
|
}
|
|
109
115
|
// ── v2.1.0: AST 内存缓存 ──────────────────────────────────────────────
|
|
110
|
-
/** 获取缓存的 AST
|
|
116
|
+
/** 获取缓存的 AST(内存级,不持久化)— v3.2.0 增加 LRU 淘汰 */
|
|
111
117
|
getAst(filePath, content) {
|
|
112
118
|
const cached = this.astCache.get(filePath);
|
|
113
119
|
if (!cached)
|
|
@@ -117,15 +123,35 @@ class SmartCache {
|
|
|
117
123
|
this.astCache.delete(filePath);
|
|
118
124
|
return undefined;
|
|
119
125
|
}
|
|
126
|
+
// v3.2.0: LRU — 访问后移到末尾(最新)
|
|
127
|
+
this.astCache.delete(filePath);
|
|
128
|
+
this.astCache.set(filePath, cached);
|
|
120
129
|
return cached.ast;
|
|
121
130
|
}
|
|
122
|
-
/** 缓存 AST 解析结果 */
|
|
131
|
+
/** 缓存 AST 解析结果 — v3.2.0 增加 LRU 淘汰 */
|
|
123
132
|
setAst(filePath, content, ast) {
|
|
133
|
+
// v3.2.0: 超出上限时淘汰最久未访问的条目(Map 头部)
|
|
134
|
+
if (this.astCache.size >= this.maxAstCacheSize && !this.astCache.has(filePath)) {
|
|
135
|
+
const firstKey = this.astCache.keys().next().value;
|
|
136
|
+
if (firstKey) {
|
|
137
|
+
this.astCache.delete(firstKey);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 删除旧条目(如果存在),然后重新插入到末尾(最新)
|
|
141
|
+
this.astCache.delete(filePath);
|
|
124
142
|
this.astCache.set(filePath, {
|
|
125
143
|
hash: SmartCache.computeHash(content),
|
|
126
144
|
ast,
|
|
127
145
|
});
|
|
128
146
|
}
|
|
147
|
+
/** v3.2.0: 获取 AST 缓存当前大小 */
|
|
148
|
+
getAstCacheSize() {
|
|
149
|
+
return this.astCache.size;
|
|
150
|
+
}
|
|
151
|
+
/** v3.2.0: 获取 AST 缓存上限 */
|
|
152
|
+
getAstCacheLimit() {
|
|
153
|
+
return this.maxAstCacheSize;
|
|
154
|
+
}
|
|
129
155
|
/** 清理过期缓存 */
|
|
130
156
|
gc() {
|
|
131
157
|
const now = Date.now();
|
package/dist/engine/cache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/engine/cache.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/engine/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -44,6 +44,8 @@ export interface EngineOptions {
|
|
|
44
44
|
skipLargeFilesThreshold?: number;
|
|
45
45
|
/** v2.6.0: 外部传入的 SmartCache 实例(用于 Watch 模式复用缓存) */
|
|
46
46
|
cacheInstance?: SmartCache;
|
|
47
|
+
/** v3.2.0: 增量扫描时通过 import 图分析扩展扫描范围(变更文件及其依赖方) */
|
|
48
|
+
incrementalImportGraph?: boolean;
|
|
47
49
|
}
|
|
48
50
|
export declare class RuleEngine {
|
|
49
51
|
private registry;
|
|
@@ -74,6 +76,14 @@ export declare class RuleEngine {
|
|
|
74
76
|
private moduleToCategory;
|
|
75
77
|
/** 执行扫描 */
|
|
76
78
|
scan(module: string): Promise<ScanResult>;
|
|
79
|
+
/**
|
|
80
|
+
* v3.3.0: 公共单文件扫描方法(用于 IDE 增量诊断)
|
|
81
|
+
* 快速扫描单个文件,返回所有匹配的 issues
|
|
82
|
+
* @param filePath 文件绝对路径
|
|
83
|
+
* @param module 模块名(可选,用于规则过滤)
|
|
84
|
+
* @returns 扫描发现的 issues
|
|
85
|
+
*/
|
|
86
|
+
scanSingleFile(filePath: string, module?: string): Promise<Issue[]>;
|
|
77
87
|
/** 扫描单个文件(带智能缓存 + 大文件跳过) */
|
|
78
88
|
private scanFile;
|
|
79
89
|
/** 获取扫描文件列表 */
|
|
@@ -82,6 +92,26 @@ export declare class RuleEngine {
|
|
|
82
92
|
private getDiffFiles;
|
|
83
93
|
/** 智能推断扫描范围:未提交修改 → 最近 5 次提交 → 全量 */
|
|
84
94
|
private getAutoScopeFiles;
|
|
95
|
+
/**
|
|
96
|
+
* 构建项目的 import 图(简化版)
|
|
97
|
+
* 对于每个文件,解析其 import 语句,建立双向映射:
|
|
98
|
+
* - importMap: file → 它导入的文件集合
|
|
99
|
+
* - reverseMap: file → 导入它的文件集合
|
|
100
|
+
*
|
|
101
|
+
* 简化策略:只解析相对路径 import(./ 或 ../),不解析 node_modules。
|
|
102
|
+
* 因为增量扫描的核心场景是:修改一个内部模块后,哪些文件会受影响。
|
|
103
|
+
*/
|
|
104
|
+
private buildImportGraph;
|
|
105
|
+
/**
|
|
106
|
+
* 基于 import 图扩展增量扫描文件列表
|
|
107
|
+
* 除变更文件本身外,还扫描所有导入这些文件的文件(上游依赖方)。
|
|
108
|
+
*
|
|
109
|
+
* 原理:如果 A 被 B import,A 修改后 B 的语义可能变化(如 A 导出的类型、常量变更)。
|
|
110
|
+
* 因此 B 也需要重新扫描。
|
|
111
|
+
*/
|
|
112
|
+
private expandIncrementalFiles;
|
|
113
|
+
/** 同步获取项目内所有源文件(用于 import 图构建) */
|
|
114
|
+
private getAllSourceFilesSync;
|
|
85
115
|
/** 创建 RuleUtils */
|
|
86
116
|
private createUtils;
|
|
87
117
|
/** 计算每行起始偏移 */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/engine/rule-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EACR,IAAI,EAEJ,KAAK,EACL,QAAQ,EACR,UAAU,EAMV,UAAU,EACb,MAAM,YAAY,CAAC;AASpB,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAEhF,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAKxC,MAAM,WAAW,aAAa;IAC1B,YAAY;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,kBAAkB;IAClB,WAAW,CAAC,EAAE,QAAQ,CAAC;IACvB,cAAc;IACd,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW;IACX,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU;IACV,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,eAAe;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oCAAoC;IACpC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,aAAa,CAAC,EAAE,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"rule-engine.d.ts","sourceRoot":"","sources":["../../src/engine/rule-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -136,8 +136,8 @@ class RuleEngine {
|
|
|
136
136
|
const files = await this.getScanFiles();
|
|
137
137
|
let filesWithIssues = 0;
|
|
138
138
|
console.log(picocolors_1.default.blue(`🔍 [${module}] 扫描 ${files.length} 个文件,${activeRules.length} 条规则...`));
|
|
139
|
-
//
|
|
140
|
-
const concurrency = this.options.concurrency ?? (0, concurrent_js_1.
|
|
139
|
+
// v3.2.0: 自适应并发 — 根据文件数、规则数和 CPU 动态调整
|
|
140
|
+
const concurrency = this.options.concurrency ?? (0, concurrent_js_1.getAdaptiveConcurrency)(files.length, activeRules.length);
|
|
141
141
|
const fileResults = await (0, concurrent_js_1.concurrentMap)(files, concurrency, (file) => this.scanFile(file, activeRules));
|
|
142
142
|
let filesSkipped = 0;
|
|
143
143
|
for (const { issues: fileIssues, skipped } of fileResults) {
|
|
@@ -189,6 +189,28 @@ class RuleEngine {
|
|
|
189
189
|
}
|
|
190
190
|
return result;
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* v3.3.0: 公共单文件扫描方法(用于 IDE 增量诊断)
|
|
194
|
+
* 快速扫描单个文件,返回所有匹配的 issues
|
|
195
|
+
* @param filePath 文件绝对路径
|
|
196
|
+
* @param module 模块名(可选,用于规则过滤)
|
|
197
|
+
* @returns 扫描发现的 issues
|
|
198
|
+
*/
|
|
199
|
+
async scanSingleFile(filePath, module) {
|
|
200
|
+
const category = module ? this.moduleToCategory(module) : undefined;
|
|
201
|
+
const activeRules = category
|
|
202
|
+
? this.filterRules({
|
|
203
|
+
category,
|
|
204
|
+
framework: this.projectMeta.framework,
|
|
205
|
+
platform: this.projectMeta.platforms[0],
|
|
206
|
+
componentLib: this.projectMeta.componentLib,
|
|
207
|
+
})
|
|
208
|
+
: this.getRules();
|
|
209
|
+
if (activeRules.length === 0)
|
|
210
|
+
return [];
|
|
211
|
+
const result = await this.scanFile(filePath, activeRules);
|
|
212
|
+
return result.issues;
|
|
213
|
+
}
|
|
192
214
|
/** 扫描单个文件(带智能缓存 + 大文件跳过) */
|
|
193
215
|
async scanFile(filePath, rules) {
|
|
194
216
|
try {
|
|
@@ -264,7 +286,15 @@ class RuleEngine {
|
|
|
264
286
|
}
|
|
265
287
|
// 过滤出符合扩展名的文件
|
|
266
288
|
const include = this.config.scan?.includeExtensions || [".js", ".ts", ".jsx", ".tsx", ".vue"];
|
|
267
|
-
|
|
289
|
+
let filtered = diffFiles.filter((f) => include.some((ext) => f.endsWith(ext)));
|
|
290
|
+
// v3.2.0: 通过 import 图分析扩展扫描范围
|
|
291
|
+
if (this.options.incrementalImportGraph !== false && filtered.length > 0 && filtered.length < 500) {
|
|
292
|
+
const expanded = this.expandIncrementalFiles(filtered, include);
|
|
293
|
+
if (expanded.length > filtered.length) {
|
|
294
|
+
console.log(picocolors_1.default.cyan(` 📎 import 图扩展: ${filtered.length} → ${expanded.length} 个文件`));
|
|
295
|
+
filtered = expanded;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
268
298
|
if (this.options.autoScope && filtered.length > 0) {
|
|
269
299
|
console.log(picocolors_1.default.cyan(`🔍 智能扫描范围: ${filtered.length} 个文件`));
|
|
270
300
|
}
|
|
@@ -367,6 +397,132 @@ class RuleEngine {
|
|
|
367
397
|
return [];
|
|
368
398
|
}
|
|
369
399
|
}
|
|
400
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
401
|
+
// v3.2.0: 增量扫描 import 图分析
|
|
402
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
403
|
+
/**
|
|
404
|
+
* 构建项目的 import 图(简化版)
|
|
405
|
+
* 对于每个文件,解析其 import 语句,建立双向映射:
|
|
406
|
+
* - importMap: file → 它导入的文件集合
|
|
407
|
+
* - reverseMap: file → 导入它的文件集合
|
|
408
|
+
*
|
|
409
|
+
* 简化策略:只解析相对路径 import(./ 或 ../),不解析 node_modules。
|
|
410
|
+
* 因为增量扫描的核心场景是:修改一个内部模块后,哪些文件会受影响。
|
|
411
|
+
*/
|
|
412
|
+
buildImportGraph(files) {
|
|
413
|
+
const importMap = new Map();
|
|
414
|
+
const reverseMap = new Map();
|
|
415
|
+
for (const file of files) {
|
|
416
|
+
importMap.set(file, new Set());
|
|
417
|
+
}
|
|
418
|
+
for (const file of files) {
|
|
419
|
+
try {
|
|
420
|
+
const source = (0, node_fs_1.readFileSync)(file, "utf-8");
|
|
421
|
+
// 简单正则提取相对路径 import(不依赖 AST,速度更快)
|
|
422
|
+
const importRegex = /import\s+.*?\s+from\s+['"](\.\.?\/[^'"]+)['"]|import\s*\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g;
|
|
423
|
+
let match;
|
|
424
|
+
while ((match = importRegex.exec(source)) !== null) {
|
|
425
|
+
const importPath = match[1] || match[2];
|
|
426
|
+
if (!importPath)
|
|
427
|
+
continue;
|
|
428
|
+
// 解析相对路径为绝对路径
|
|
429
|
+
const { resolve: pathResolve, dirname } = require("node:path");
|
|
430
|
+
let resolved = pathResolve(dirname(file), importPath);
|
|
431
|
+
// 尝试补充扩展名
|
|
432
|
+
const exts = [".ts", ".tsx", ".js", ".jsx", ".vue"];
|
|
433
|
+
let found = false;
|
|
434
|
+
for (const ext of exts) {
|
|
435
|
+
if (files.includes(resolved + ext)) {
|
|
436
|
+
resolved = resolved + ext;
|
|
437
|
+
found = true;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
// 支持目录下的 index 文件
|
|
441
|
+
if (files.includes(pathResolve(resolved, "index" + ext))) {
|
|
442
|
+
resolved = pathResolve(resolved, "index" + ext);
|
|
443
|
+
found = true;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (found) {
|
|
448
|
+
importMap.get(file)?.add(resolved);
|
|
449
|
+
if (!reverseMap.has(resolved)) {
|
|
450
|
+
reverseMap.set(resolved, new Set());
|
|
451
|
+
}
|
|
452
|
+
reverseMap.get(resolved).add(file);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// 读取失败则跳过
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return { importMap, reverseMap };
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 基于 import 图扩展增量扫描文件列表
|
|
464
|
+
* 除变更文件本身外,还扫描所有导入这些文件的文件(上游依赖方)。
|
|
465
|
+
*
|
|
466
|
+
* 原理:如果 A 被 B import,A 修改后 B 的语义可能变化(如 A 导出的类型、常量变更)。
|
|
467
|
+
* 因此 B 也需要重新扫描。
|
|
468
|
+
*/
|
|
469
|
+
expandIncrementalFiles(changedFiles, includeExts) {
|
|
470
|
+
try {
|
|
471
|
+
// 1. 获取项目内所有候选文件(用于构建 import 图)
|
|
472
|
+
const allFiles = this.getAllSourceFilesSync(includeExts);
|
|
473
|
+
if (allFiles.length > 5000) {
|
|
474
|
+
// 超大项目:import 图构建成本高,跳过扩展
|
|
475
|
+
return changedFiles;
|
|
476
|
+
}
|
|
477
|
+
// 2. 构建 import 图
|
|
478
|
+
const { reverseMap } = this.buildImportGraph(allFiles);
|
|
479
|
+
// 3. 收集变更文件及其所有导入方
|
|
480
|
+
const expanded = new Set(changedFiles);
|
|
481
|
+
const queue = [...changedFiles];
|
|
482
|
+
const visited = new Set(changedFiles);
|
|
483
|
+
while (queue.length > 0) {
|
|
484
|
+
const current = queue.shift();
|
|
485
|
+
const importers = reverseMap.get(current);
|
|
486
|
+
if (!importers)
|
|
487
|
+
continue;
|
|
488
|
+
for (const importer of importers) {
|
|
489
|
+
if (!visited.has(importer)) {
|
|
490
|
+
visited.add(importer);
|
|
491
|
+
expanded.add(importer);
|
|
492
|
+
queue.push(importer);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return Array.from(expanded);
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return changedFiles;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** 同步获取项目内所有源文件(用于 import 图构建) */
|
|
503
|
+
getAllSourceFilesSync(includeExts) {
|
|
504
|
+
try {
|
|
505
|
+
const { globbySync } = require("globby");
|
|
506
|
+
const patterns = includeExts.map((ext) => `**/*${ext}`);
|
|
507
|
+
const exclude = [
|
|
508
|
+
"**/node_modules/**",
|
|
509
|
+
"**/dist/**",
|
|
510
|
+
"**/build/**",
|
|
511
|
+
"**/.git/**",
|
|
512
|
+
"**/coverage/**",
|
|
513
|
+
...(this.options.exclude || []),
|
|
514
|
+
...(this.config.scan?.excludeDirs?.map((d) => `**/${d}/**`) || []),
|
|
515
|
+
];
|
|
516
|
+
return globbySync(patterns, {
|
|
517
|
+
cwd: this.options.projectDir,
|
|
518
|
+
ignore: exclude,
|
|
519
|
+
absolute: true,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return [];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
370
526
|
/** 创建 RuleUtils */
|
|
371
527
|
createUtils(filePath, source) {
|
|
372
528
|
const lineOffsets = this.computeLineOffsets(source);
|