repowiki-core 0.1.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/dist/analyzer/api-analyzer.d.ts +22 -0
- package/dist/analyzer/api-analyzer.d.ts.map +1 -0
- package/dist/analyzer/api-analyzer.js +272 -0
- package/dist/analyzer/api-analyzer.js.map +1 -0
- package/dist/analyzer/config-analyzer.d.ts +18 -0
- package/dist/analyzer/config-analyzer.d.ts.map +1 -0
- package/dist/analyzer/config-analyzer.js +200 -0
- package/dist/analyzer/config-analyzer.js.map +1 -0
- package/dist/analyzer/database-analyzer.d.ts +24 -0
- package/dist/analyzer/database-analyzer.d.ts.map +1 -0
- package/dist/analyzer/database-analyzer.js +391 -0
- package/dist/analyzer/database-analyzer.js.map +1 -0
- package/dist/analyzer/index.d.ts +10 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +10 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/module-analyzer.d.ts +20 -0
- package/dist/analyzer/module-analyzer.d.ts.map +1 -0
- package/dist/analyzer/module-analyzer.js +252 -0
- package/dist/analyzer/module-analyzer.js.map +1 -0
- package/dist/analyzer/workflow-analyzer.d.ts +19 -0
- package/dist/analyzer/workflow-analyzer.d.ts.map +1 -0
- package/dist/analyzer/workflow-analyzer.js +165 -0
- package/dist/analyzer/workflow-analyzer.js.map +1 -0
- package/dist/detector/dependency-detector.d.ts +50 -0
- package/dist/detector/dependency-detector.d.ts.map +1 -0
- package/dist/detector/dependency-detector.js +326 -0
- package/dist/detector/dependency-detector.js.map +1 -0
- package/dist/detector/entrypoint-detector.d.ts +30 -0
- package/dist/detector/entrypoint-detector.d.ts.map +1 -0
- package/dist/detector/entrypoint-detector.js +240 -0
- package/dist/detector/entrypoint-detector.js.map +1 -0
- package/dist/detector/index.d.ts +10 -0
- package/dist/detector/index.d.ts.map +1 -0
- package/dist/detector/index.js +10 -0
- package/dist/detector/index.js.map +1 -0
- package/dist/detector/tech-stack-detector.d.ts +41 -0
- package/dist/detector/tech-stack-detector.d.ts.map +1 -0
- package/dist/detector/tech-stack-detector.js +300 -0
- package/dist/detector/tech-stack-detector.js.map +1 -0
- package/dist/generator/index.d.ts +9 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +9 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/markdown-generator.d.ts +71 -0
- package/dist/generator/markdown-generator.d.ts.map +1 -0
- package/dist/generator/markdown-generator.js +235 -0
- package/dist/generator/markdown-generator.js.map +1 -0
- package/dist/generator/mermaid-generator.d.ts +30 -0
- package/dist/generator/mermaid-generator.d.ts.map +1 -0
- package/dist/generator/mermaid-generator.js +297 -0
- package/dist/generator/mermaid-generator.js.map +1 -0
- package/dist/generator/sidebar-generator.d.ts +10 -0
- package/dist/generator/sidebar-generator.d.ts.map +1 -0
- package/dist/generator/sidebar-generator.js +120 -0
- package/dist/generator/sidebar-generator.js.map +1 -0
- package/dist/generator/wiki-generator.d.ts +45 -0
- package/dist/generator/wiki-generator.d.ts.map +1 -0
- package/dist/generator/wiki-generator.js +217 -0
- package/dist/generator/wiki-generator.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/auth-manager.d.ts +50 -0
- package/dist/llm/auth-manager.d.ts.map +1 -0
- package/dist/llm/auth-manager.js +172 -0
- package/dist/llm/auth-manager.js.map +1 -0
- package/dist/llm/index.d.ts +10 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +9 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/llm-client.d.ts +132 -0
- package/dist/llm/llm-client.d.ts.map +1 -0
- package/dist/llm/llm-client.js +308 -0
- package/dist/llm/llm-client.js.map +1 -0
- package/dist/llm/prompt-manager.d.ts +67 -0
- package/dist/llm/prompt-manager.d.ts.map +1 -0
- package/dist/llm/prompt-manager.js +283 -0
- package/dist/llm/prompt-manager.js.map +1 -0
- package/dist/models/analysis-result.d.ts +425 -0
- package/dist/models/analysis-result.d.ts.map +1 -0
- package/dist/models/analysis-result.js +34 -0
- package/dist/models/analysis-result.js.map +1 -0
- package/dist/models/analysis-types.d.ts +223 -0
- package/dist/models/analysis-types.d.ts.map +1 -0
- package/dist/models/analysis-types.js +95 -0
- package/dist/models/analysis-types.js.map +1 -0
- package/dist/models/file-reference.d.ts +62 -0
- package/dist/models/file-reference.d.ts.map +1 -0
- package/dist/models/file-reference.js +34 -0
- package/dist/models/file-reference.js.map +1 -0
- package/dist/models/index.d.ts +10 -0
- package/dist/models/index.d.ts.map +1 -0
- package/dist/models/index.js +10 -0
- package/dist/models/index.js.map +1 -0
- package/dist/models/project-profile.d.ts +48 -0
- package/dist/models/project-profile.d.ts.map +1 -0
- package/dist/models/project-profile.js +26 -0
- package/dist/models/project-profile.js.map +1 -0
- package/dist/models/wiki-page.d.ts +57 -0
- package/dist/models/wiki-page.d.ts.map +1 -0
- package/dist/models/wiki-page.js +19 -0
- package/dist/models/wiki-page.js.map +1 -0
- package/dist/pipeline.d.ts +30 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +159 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/scanner/file-scanner.d.ts +27 -0
- package/dist/scanner/file-scanner.d.ts.map +1 -0
- package/dist/scanner/file-scanner.js +149 -0
- package/dist/scanner/file-scanner.js.map +1 -0
- package/dist/scanner/ignore-rules.d.ts +31 -0
- package/dist/scanner/ignore-rules.d.ts.map +1 -0
- package/dist/scanner/ignore-rules.js +98 -0
- package/dist/scanner/ignore-rules.js.map +1 -0
- package/dist/scanner/index.d.ts +8 -0
- package/dist/scanner/index.d.ts.map +1 -0
- package/dist/scanner/index.js +8 -0
- package/dist/scanner/index.js.map +1 -0
- package/dist/scanner/tree-builder.d.ts +20 -0
- package/dist/scanner/tree-builder.d.ts.map +1 -0
- package/dist/scanner/tree-builder.js +118 -0
- package/dist/scanner/tree-builder.js.map +1 -0
- package/package.json +34 -0
- package/src/analyzer/api-analyzer.ts +324 -0
- package/src/analyzer/config-analyzer.ts +209 -0
- package/src/analyzer/database-analyzer.ts +468 -0
- package/src/analyzer/index.ts +26 -0
- package/src/analyzer/module-analyzer.ts +308 -0
- package/src/analyzer/workflow-analyzer.ts +190 -0
- package/src/detector/dependency-detector.ts +390 -0
- package/src/detector/entrypoint-detector.ts +270 -0
- package/src/detector/index.ts +21 -0
- package/src/detector/tech-stack-detector.ts +377 -0
- package/src/generator/index.ts +36 -0
- package/src/generator/markdown-generator.ts +277 -0
- package/src/generator/mermaid-generator.ts +340 -0
- package/src/generator/sidebar-generator.ts +134 -0
- package/src/generator/wiki-generator.ts +281 -0
- package/src/index.ts +12 -0
- package/src/llm/auth-manager.ts +207 -0
- package/src/llm/index.ts +21 -0
- package/src/llm/llm-client.ts +417 -0
- package/src/llm/prompt-manager.ts +325 -0
- package/src/models/analysis-result.ts +44 -0
- package/src/models/analysis-types.ts +121 -0
- package/src/models/file-reference.ts +41 -0
- package/src/models/index.ts +44 -0
- package/src/models/project-profile.ts +29 -0
- package/src/models/wiki-page.ts +23 -0
- package/src/pipeline.ts +225 -0
- package/src/scanner/file-scanner.ts +192 -0
- package/src/scanner/ignore-rules.ts +112 -0
- package/src/scanner/index.ts +19 -0
- package/src/scanner/tree-builder.ts +156 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module detector/dependency-detector
|
|
3
|
+
* 依赖关系探测器
|
|
4
|
+
*
|
|
5
|
+
* 通过正则表达式解析源文件中的 import / require 语句,
|
|
6
|
+
* 构建项目内部模块间的依赖图以及外部包引用列表。
|
|
7
|
+
*
|
|
8
|
+
* 为避免性能问题,仅处理前 500 个源文件。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs/promises';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import type { FileNode } from '../models/index.js';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// 公开接口
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
/** 依赖边,表示一个文件对另一个模块的引用 */
|
|
20
|
+
export interface DependencyEdge {
|
|
21
|
+
/** 源文件的相对路径 */
|
|
22
|
+
source: string;
|
|
23
|
+
/** 引用目标的相对路径(内部模块)或包名(外部依赖) */
|
|
24
|
+
target: string;
|
|
25
|
+
/** 是否为外部依赖 */
|
|
26
|
+
isExternal: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 完整的依赖关系图 */
|
|
30
|
+
export interface DependencyGraph {
|
|
31
|
+
/** 所有依赖边 */
|
|
32
|
+
edges: DependencyEdge[];
|
|
33
|
+
/** 参与图中的内部模块路径列表 */
|
|
34
|
+
internalModules: string[];
|
|
35
|
+
/** 引用到的外部包名列表 */
|
|
36
|
+
externalPackages: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// 常量
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/** 源文件最大处理数量,超过后截断以保障性能 */
|
|
44
|
+
const MAX_SOURCE_FILES = 500;
|
|
45
|
+
|
|
46
|
+
/** 待分析的源文件扩展名集合 */
|
|
47
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.py']);
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// 正则表达式
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* TypeScript / JavaScript 导入语句匹配模式。
|
|
55
|
+
*
|
|
56
|
+
* 覆盖的语法形式:
|
|
57
|
+
* - `import ... from 'module'`
|
|
58
|
+
* - `import 'module'`(副作用导入)
|
|
59
|
+
* - `require('module')`
|
|
60
|
+
* - `import('module')`(动态导入)
|
|
61
|
+
*/
|
|
62
|
+
const TS_IMPORT_PATTERNS: RegExp[] = [
|
|
63
|
+
// import ... from 'module' 或 import 'module'
|
|
64
|
+
/import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
65
|
+
// require('module')
|
|
66
|
+
/require\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
67
|
+
// import('module') — 动态导入
|
|
68
|
+
/import\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Python 导入语句匹配模式。
|
|
73
|
+
*
|
|
74
|
+
* 覆盖的语法形式:
|
|
75
|
+
* - `import module`
|
|
76
|
+
* - `from module import ...`
|
|
77
|
+
*/
|
|
78
|
+
const PY_IMPORT_PATTERNS: RegExp[] = [
|
|
79
|
+
/^import\s+(\S+)/gm,
|
|
80
|
+
/^from\s+(\S+)\s+import/gm,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// 辅助函数
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 安全读取文件内容,文件不存在或读取失败时返回 null。
|
|
89
|
+
*/
|
|
90
|
+
async function safeReadFile(filePath: string): Promise<string | null> {
|
|
91
|
+
try {
|
|
92
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 规范化路径分隔符为 POSIX 风格。
|
|
100
|
+
*/
|
|
101
|
+
function normalizePath(p: string): string {
|
|
102
|
+
return p.replace(/\\/g, '/');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 判断导入路径是否为内部模块引用。
|
|
107
|
+
*
|
|
108
|
+
* 内部模块的导入路径以 `.` 或 `/` 开头。
|
|
109
|
+
* Python 的相对导入也以 `.` 开头。
|
|
110
|
+
*
|
|
111
|
+
* @param importPath 导入路径字符串
|
|
112
|
+
*/
|
|
113
|
+
function isInternalImport(importPath: string): boolean {
|
|
114
|
+
return importPath.startsWith('.') || importPath.startsWith('/');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 从导入路径中提取顶层外部包名。
|
|
119
|
+
*
|
|
120
|
+
* 例如:
|
|
121
|
+
* - `@nestjs/core` → `@nestjs/core`(scoped 包保留前两级)
|
|
122
|
+
* - `express/lib/router` → `express`
|
|
123
|
+
* - `lodash` → `lodash`
|
|
124
|
+
*
|
|
125
|
+
* @param importPath 导入路径
|
|
126
|
+
*/
|
|
127
|
+
function extractPackageName(importPath: string): string {
|
|
128
|
+
if (importPath.startsWith('@')) {
|
|
129
|
+
// scoped package: @scope/name
|
|
130
|
+
const parts = importPath.split('/');
|
|
131
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
132
|
+
}
|
|
133
|
+
return importPath.split('/')[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 从 Python 导入路径中提取顶层包名。
|
|
138
|
+
*
|
|
139
|
+
* 例如:
|
|
140
|
+
* - `fastapi.middleware.cors` → `fastapi`
|
|
141
|
+
* - `app.main` → `app`
|
|
142
|
+
*
|
|
143
|
+
* @param importPath Python 模块路径
|
|
144
|
+
*/
|
|
145
|
+
function extractPythonPackageName(importPath: string): string {
|
|
146
|
+
return importPath.split('.')[0];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 判断 Python 导入是否为内部模块。
|
|
151
|
+
*
|
|
152
|
+
* 以 `.` 开头的是显式相对导入;其余通过检查是否为项目内文件来判断。
|
|
153
|
+
*
|
|
154
|
+
* @param importPath Python 导入路径
|
|
155
|
+
* @param internalTopDirs 项目中存在的顶层目录名集合
|
|
156
|
+
*/
|
|
157
|
+
function isPythonInternalImport(importPath: string, internalTopDirs: Set<string>): boolean {
|
|
158
|
+
if (importPath.startsWith('.')) return true;
|
|
159
|
+
const topLevel = extractPythonPackageName(importPath);
|
|
160
|
+
return internalTopDirs.has(topLevel);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 尝试将内部导入路径解析为实际文件的相对路径。
|
|
165
|
+
*
|
|
166
|
+
* 对于 TS/JS 模块,依次尝试:
|
|
167
|
+
* 1. 直接路径
|
|
168
|
+
* 2. 追加 .ts / .tsx / .js / .jsx
|
|
169
|
+
* 3. 追加 /index.ts 等
|
|
170
|
+
*
|
|
171
|
+
* @param sourceFile 导入语句所在的源文件路径
|
|
172
|
+
* @param importPath 导入路径
|
|
173
|
+
* @param fileSet 项目中所有文件的相对路径集合
|
|
174
|
+
*/
|
|
175
|
+
function resolveInternalPath(
|
|
176
|
+
sourceFile: string,
|
|
177
|
+
importPath: string,
|
|
178
|
+
fileSet: Set<string>,
|
|
179
|
+
): string | null {
|
|
180
|
+
const sourceDir = path.posix.dirname(sourceFile);
|
|
181
|
+
const resolved = path.posix.normalize(path.posix.join(sourceDir, importPath));
|
|
182
|
+
|
|
183
|
+
// 直接匹配
|
|
184
|
+
if (fileSet.has(resolved)) return resolved;
|
|
185
|
+
|
|
186
|
+
// 尝试常见扩展名
|
|
187
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
188
|
+
for (const ext of extensions) {
|
|
189
|
+
const withExt = resolved + ext;
|
|
190
|
+
if (fileSet.has(withExt)) return withExt;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 尝试 index 文件
|
|
194
|
+
for (const ext of extensions) {
|
|
195
|
+
const indexPath = path.posix.join(resolved, `index${ext}`);
|
|
196
|
+
if (fileSet.has(indexPath)) return indexPath;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 无法解析时返回规范化后的路径
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 尝试将 Python 内部导入解析为文件路径。
|
|
205
|
+
*
|
|
206
|
+
* @param importPath Python 导入路径(点号分隔)
|
|
207
|
+
* @param fileSet 项目中所有文件的相对路径集合
|
|
208
|
+
*/
|
|
209
|
+
function resolvePythonInternalPath(
|
|
210
|
+
importPath: string,
|
|
211
|
+
fileSet: Set<string>,
|
|
212
|
+
): string | null {
|
|
213
|
+
// 跳过以 . 开头的相对导入(需要源文件位置才能解析,暂简化处理)
|
|
214
|
+
if (importPath.startsWith('.')) return null;
|
|
215
|
+
|
|
216
|
+
const asFilePath = importPath.replace(/\./g, '/');
|
|
217
|
+
|
|
218
|
+
// 尝试直接 .py 文件
|
|
219
|
+
const pyPath = asFilePath + '.py';
|
|
220
|
+
if (fileSet.has(pyPath)) return pyPath;
|
|
221
|
+
|
|
222
|
+
// 尝试 __init__.py
|
|
223
|
+
const initPath = path.posix.join(asFilePath, '__init__.py');
|
|
224
|
+
if (fileSet.has(initPath)) return initPath;
|
|
225
|
+
|
|
226
|
+
return asFilePath + '.py';
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// 提取器
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 从 TypeScript/JavaScript 源文件中提取所有导入路径。
|
|
235
|
+
*
|
|
236
|
+
* @param content 文件内容
|
|
237
|
+
* @returns 导入路径列表
|
|
238
|
+
*/
|
|
239
|
+
function extractTsImports(content: string): string[] {
|
|
240
|
+
const imports: string[] = [];
|
|
241
|
+
for (const pattern of TS_IMPORT_PATTERNS) {
|
|
242
|
+
// 重置 lastIndex,因为使用了 g 标志
|
|
243
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
244
|
+
let match: RegExpExecArray | null;
|
|
245
|
+
while ((match = regex.exec(content)) !== null) {
|
|
246
|
+
imports.push(match[1]);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return imports;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* 从 Python 源文件中提取所有导入路径。
|
|
254
|
+
*
|
|
255
|
+
* @param content 文件内容
|
|
256
|
+
* @returns 导入路径列表
|
|
257
|
+
*/
|
|
258
|
+
function extractPyImports(content: string): string[] {
|
|
259
|
+
const imports: string[] = [];
|
|
260
|
+
for (const pattern of PY_IMPORT_PATTERNS) {
|
|
261
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
262
|
+
let match: RegExpExecArray | null;
|
|
263
|
+
while ((match = regex.exec(content)) !== null) {
|
|
264
|
+
imports.push(match[1]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return imports;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// 主入口
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 构建项目依赖关系图。
|
|
276
|
+
*
|
|
277
|
+
* 扫描源文件(.ts, .tsx, .js, .jsx, .py)中的 import/require 语句,
|
|
278
|
+
* 将导入分类为内部模块引用和外部包引用,尝试解析内部导入的实际文件路径,
|
|
279
|
+
* 并汇总为完整的依赖图。
|
|
280
|
+
*
|
|
281
|
+
* 为避免性能问题,最多处理前 {@link MAX_SOURCE_FILES} 个源文件。
|
|
282
|
+
*
|
|
283
|
+
* @param rootPath 项目根目录的绝对路径
|
|
284
|
+
* @param files FileScanner 输出的文件节点列表
|
|
285
|
+
* @returns 完整的依赖关系图
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* const graph = await buildDependencyGraph('/path/to/project', files);
|
|
290
|
+
* console.log(graph.externalPackages); // ['react', 'express', ...]
|
|
291
|
+
* console.log(graph.edges.length); // 依赖边数量
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
export async function buildDependencyGraph(
|
|
295
|
+
rootPath: string,
|
|
296
|
+
files: FileNode[],
|
|
297
|
+
): Promise<DependencyGraph> {
|
|
298
|
+
const edges: DependencyEdge[] = [];
|
|
299
|
+
const internalModulesSet = new Set<string>();
|
|
300
|
+
const externalPackagesSet = new Set<string>();
|
|
301
|
+
|
|
302
|
+
// 构建文件路径集合用于内部模块解析
|
|
303
|
+
const fileSet = new Set<string>(
|
|
304
|
+
files
|
|
305
|
+
.filter((f) => f.nodeType === 'file')
|
|
306
|
+
.map((f) => normalizePath(f.relativePath)),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// 收集项目顶层目录名,用于 Python 内部导入判断
|
|
310
|
+
const internalTopDirs = new Set<string>();
|
|
311
|
+
for (const f of files) {
|
|
312
|
+
const rel = normalizePath(f.relativePath);
|
|
313
|
+
const topDir = rel.split('/')[0];
|
|
314
|
+
if (topDir) {
|
|
315
|
+
internalTopDirs.add(topDir);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 筛选源文件,限制处理数量
|
|
320
|
+
const sourceFiles = files
|
|
321
|
+
.filter((f) => {
|
|
322
|
+
if (f.nodeType !== 'file') return false;
|
|
323
|
+
const ext = path.extname(f.relativePath).toLowerCase();
|
|
324
|
+
return SOURCE_EXTENSIONS.has(ext);
|
|
325
|
+
})
|
|
326
|
+
.slice(0, MAX_SOURCE_FILES);
|
|
327
|
+
|
|
328
|
+
// 并行读取所有源文件内容
|
|
329
|
+
const readResults = await Promise.all(
|
|
330
|
+
sourceFiles.map(async (f) => {
|
|
331
|
+
const absolutePath = path.join(rootPath, f.relativePath);
|
|
332
|
+
const content = await safeReadFile(absolutePath);
|
|
333
|
+
return { file: f, content };
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// 逐文件解析导入语句
|
|
338
|
+
for (const { file, content } of readResults) {
|
|
339
|
+
if (!content) continue;
|
|
340
|
+
|
|
341
|
+
const relativePath = normalizePath(file.relativePath);
|
|
342
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
343
|
+
const isPython = ext === '.py';
|
|
344
|
+
|
|
345
|
+
// 提取导入路径
|
|
346
|
+
const importPaths = isPython
|
|
347
|
+
? extractPyImports(content)
|
|
348
|
+
: extractTsImports(content);
|
|
349
|
+
|
|
350
|
+
for (const importPath of importPaths) {
|
|
351
|
+
if (isPython) {
|
|
352
|
+
// Python 导入处理
|
|
353
|
+
const isInternal = isPythonInternalImport(importPath, internalTopDirs);
|
|
354
|
+
|
|
355
|
+
if (isInternal) {
|
|
356
|
+
const resolved = resolvePythonInternalPath(importPath, fileSet);
|
|
357
|
+
const target = resolved ?? importPath.replace(/\./g, '/') + '.py';
|
|
358
|
+
edges.push({ source: relativePath, target, isExternal: false });
|
|
359
|
+
internalModulesSet.add(relativePath);
|
|
360
|
+
internalModulesSet.add(target);
|
|
361
|
+
} else {
|
|
362
|
+
const pkgName = extractPythonPackageName(importPath);
|
|
363
|
+
edges.push({ source: relativePath, target: pkgName, isExternal: true });
|
|
364
|
+
externalPackagesSet.add(pkgName);
|
|
365
|
+
internalModulesSet.add(relativePath);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
// TypeScript / JavaScript 导入处理
|
|
369
|
+
if (isInternalImport(importPath)) {
|
|
370
|
+
const resolved = resolveInternalPath(relativePath, importPath, fileSet);
|
|
371
|
+
const target = resolved ?? importPath;
|
|
372
|
+
edges.push({ source: relativePath, target, isExternal: false });
|
|
373
|
+
internalModulesSet.add(relativePath);
|
|
374
|
+
internalModulesSet.add(target);
|
|
375
|
+
} else {
|
|
376
|
+
const pkgName = extractPackageName(importPath);
|
|
377
|
+
edges.push({ source: relativePath, target: pkgName, isExternal: true });
|
|
378
|
+
externalPackagesSet.add(pkgName);
|
|
379
|
+
internalModulesSet.add(relativePath);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
edges,
|
|
387
|
+
internalModules: [...internalModulesSet].sort(),
|
|
388
|
+
externalPackages: [...externalPackagesSet].sort(),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module detector/entrypoint-detector
|
|
3
|
+
* 入口文件探测器
|
|
4
|
+
*
|
|
5
|
+
* 通过启发式规则(常见入口文件名、package.json scripts、Dockerfile CMD/ENTRYPOINT)
|
|
6
|
+
* 自动定位项目可能的启动入口。这些信息对后续的调用图分析至关重要。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'node:fs/promises';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import type { FileNode } from '../models/index.js';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// 常见入口文件路径(按优先级排列)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const COMMON_ENTRYPOINTS: string[] = [
|
|
18
|
+
// Python 入口
|
|
19
|
+
'main.py',
|
|
20
|
+
'app/main.py',
|
|
21
|
+
'apps/api/app/main.py',
|
|
22
|
+
// TypeScript / JavaScript 入口
|
|
23
|
+
'src/main.ts',
|
|
24
|
+
'src/index.ts',
|
|
25
|
+
'src/app.ts',
|
|
26
|
+
// React 入口
|
|
27
|
+
'src/App.tsx',
|
|
28
|
+
'src/App.jsx',
|
|
29
|
+
// Next.js / Pages 路由入口
|
|
30
|
+
'app/page.tsx',
|
|
31
|
+
'pages/index.tsx',
|
|
32
|
+
'app/layout.tsx',
|
|
33
|
+
// 静态站点入口
|
|
34
|
+
'index.html',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// 辅助函数
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 安全读取文件内容,文件不存在时返回 null。
|
|
43
|
+
* @param filePath 要读取的文件绝对路径
|
|
44
|
+
*/
|
|
45
|
+
async function safeReadFile(filePath: string): Promise<string | null> {
|
|
46
|
+
try {
|
|
47
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 规范化路径分隔符为 POSIX 风格的正斜杠。
|
|
55
|
+
* @param p 待规范化的路径
|
|
56
|
+
*/
|
|
57
|
+
function normalizePath(p: string): string {
|
|
58
|
+
return p.replace(/\\/g, '/');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 检查文件节点列表中是否包含指定相对路径的文件。
|
|
63
|
+
* @param files 文件节点列表
|
|
64
|
+
* @param relativePath 相对路径
|
|
65
|
+
*/
|
|
66
|
+
function fileExists(files: FileNode[], relativePath: string): boolean {
|
|
67
|
+
const normalized = normalizePath(relativePath);
|
|
68
|
+
return files.some(
|
|
69
|
+
(f) => f.nodeType === 'file' && normalizePath(f.relativePath) === normalized,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 从 npm script 命令字符串中提取可能的文件路径。
|
|
75
|
+
*
|
|
76
|
+
* 支持的模式:
|
|
77
|
+
* - `node dist/main.js`
|
|
78
|
+
* - `ts-node src/index.ts`
|
|
79
|
+
* - `tsx src/main.ts`
|
|
80
|
+
* - `python app/main.py`
|
|
81
|
+
* - `uvicorn app.main:app` → `app/main.py`
|
|
82
|
+
* - `next dev` → 无文件路径,跳过
|
|
83
|
+
*
|
|
84
|
+
* @param script 脚本命令字符串
|
|
85
|
+
* @returns 提取到的文件路径列表
|
|
86
|
+
*/
|
|
87
|
+
function extractPathsFromScript(script: string): string[] {
|
|
88
|
+
const paths: string[] = [];
|
|
89
|
+
|
|
90
|
+
// 匹配显式文件路径参数:node/ts-node/tsx/python 后跟文件路径
|
|
91
|
+
const explicitFilePattern = /(?:node|ts-node|tsx|python|python3)\s+([\w./-]+\.\w+)/gi;
|
|
92
|
+
let match: RegExpExecArray | null;
|
|
93
|
+
while ((match = explicitFilePattern.exec(script)) !== null) {
|
|
94
|
+
paths.push(match[1]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 匹配 uvicorn 风格的模块路径:uvicorn app.main:app
|
|
98
|
+
const uvicornPattern = /uvicorn\s+([\w.]+):/gi;
|
|
99
|
+
while ((match = uvicornPattern.exec(script)) !== null) {
|
|
100
|
+
// 将点号分隔的模块路径转换为文件路径
|
|
101
|
+
const modulePath = match[1].replace(/\./g, '/') + '.py';
|
|
102
|
+
paths.push(modulePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// 检测子流程
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 检查常见入口文件名是否存在于文件列表中。
|
|
114
|
+
*/
|
|
115
|
+
function detectCommonEntrypoints(files: FileNode[]): string[] {
|
|
116
|
+
const found: string[] = [];
|
|
117
|
+
for (const entry of COMMON_ENTRYPOINTS) {
|
|
118
|
+
if (fileExists(files, entry)) {
|
|
119
|
+
found.push(entry);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return found;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 从 package.json 的 scripts 字段提取入口文件路径。
|
|
127
|
+
* 仅解析 start、dev、serve 三个常见启动脚本。
|
|
128
|
+
*/
|
|
129
|
+
async function detectFromPackageJsonScripts(rootPath: string): Promise<string[]> {
|
|
130
|
+
const content = await safeReadFile(path.join(rootPath, 'package.json'));
|
|
131
|
+
if (!content) return [];
|
|
132
|
+
|
|
133
|
+
let pkg: Record<string, unknown>;
|
|
134
|
+
try {
|
|
135
|
+
pkg = JSON.parse(content) as Record<string, unknown>;
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
141
|
+
if (!scripts || typeof scripts !== 'object') return [];
|
|
142
|
+
|
|
143
|
+
const targetScripts = ['start', 'dev', 'serve'];
|
|
144
|
+
const paths: string[] = [];
|
|
145
|
+
|
|
146
|
+
for (const scriptName of targetScripts) {
|
|
147
|
+
const scriptValue = scripts[scriptName];
|
|
148
|
+
if (typeof scriptValue === 'string') {
|
|
149
|
+
paths.push(...extractPathsFromScript(scriptValue));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return paths;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 从 Dockerfile 中提取 CMD 和 ENTRYPOINT 指定的文件路径。
|
|
158
|
+
*
|
|
159
|
+
* 支持的格式:
|
|
160
|
+
* - `CMD ["node", "dist/main.js"]`(exec 格式)
|
|
161
|
+
* - `CMD node dist/main.js`(shell 格式)
|
|
162
|
+
* - `ENTRYPOINT ["python", "main.py"]`
|
|
163
|
+
*/
|
|
164
|
+
async function detectFromDockerfile(rootPath: string): Promise<string[]> {
|
|
165
|
+
const content = await safeReadFile(path.join(rootPath, 'Dockerfile'));
|
|
166
|
+
if (!content) return [];
|
|
167
|
+
|
|
168
|
+
const paths: string[] = [];
|
|
169
|
+
const lines = content.split('\n');
|
|
170
|
+
|
|
171
|
+
for (const line of lines) {
|
|
172
|
+
const trimmed = line.trim();
|
|
173
|
+
|
|
174
|
+
// 跳过注释行
|
|
175
|
+
if (trimmed.startsWith('#')) continue;
|
|
176
|
+
|
|
177
|
+
// 匹配 CMD 和 ENTRYPOINT 指令
|
|
178
|
+
const instructionMatch = /^(?:CMD|ENTRYPOINT)\s+(.+)$/i.exec(trimmed);
|
|
179
|
+
if (!instructionMatch) continue;
|
|
180
|
+
|
|
181
|
+
const args = instructionMatch[1].trim();
|
|
182
|
+
|
|
183
|
+
// exec 格式:CMD ["executable", "arg1", ...]
|
|
184
|
+
if (args.startsWith('[')) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(args) as string[];
|
|
187
|
+
// 查找类似文件路径的参数(包含扩展名)
|
|
188
|
+
for (const arg of parsed) {
|
|
189
|
+
if (/\.\w+$/.test(arg) && !arg.startsWith('-')) {
|
|
190
|
+
paths.push(arg);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// JSON 解析失败则忽略
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// shell 格式:CMD node dist/main.js
|
|
198
|
+
const tokens = args.split(/\s+/);
|
|
199
|
+
for (const token of tokens) {
|
|
200
|
+
if (/\.\w+$/.test(token) && !token.startsWith('-')) {
|
|
201
|
+
paths.push(token);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return paths;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// 主入口
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 检测项目入口文件。
|
|
216
|
+
*
|
|
217
|
+
* 按照以下优先级顺序查找:
|
|
218
|
+
* 1. 常见入口文件名
|
|
219
|
+
* 2. package.json scripts 字段
|
|
220
|
+
* 3. Dockerfile CMD/ENTRYPOINT
|
|
221
|
+
*
|
|
222
|
+
* 返回有序的入口文件相对路径列表(已去重)。
|
|
223
|
+
*
|
|
224
|
+
* @param rootPath 项目根目录的绝对路径
|
|
225
|
+
* @param files FileScanner 输出的文件节点列表
|
|
226
|
+
* @returns 入口文件的相对路径列表
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* const entrypoints = await detectEntrypoints('/path/to/project', files);
|
|
231
|
+
* // ['src/main.ts', 'app/page.tsx']
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export async function detectEntrypoints(
|
|
235
|
+
rootPath: string,
|
|
236
|
+
files: FileNode[],
|
|
237
|
+
): Promise<string[]> {
|
|
238
|
+
const result: string[] = [];
|
|
239
|
+
const seen = new Set<string>();
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 向结果中添加路径,自动去重并规范化。
|
|
243
|
+
*/
|
|
244
|
+
function addPath(p: string): void {
|
|
245
|
+
const normalized = normalizePath(p);
|
|
246
|
+
if (!seen.has(normalized)) {
|
|
247
|
+
seen.add(normalized);
|
|
248
|
+
result.push(normalized);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 1. 常见入口文件
|
|
253
|
+
for (const entry of detectCommonEntrypoints(files)) {
|
|
254
|
+
addPath(entry);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 2. package.json scripts
|
|
258
|
+
const scriptPaths = await detectFromPackageJsonScripts(rootPath);
|
|
259
|
+
for (const p of scriptPaths) {
|
|
260
|
+
addPath(p);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 3. Dockerfile CMD/ENTRYPOINT
|
|
264
|
+
const dockerPaths = await detectFromDockerfile(rootPath);
|
|
265
|
+
for (const p of dockerPaths) {
|
|
266
|
+
addPath(p);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module detector
|
|
3
|
+
* 探测器模块入口
|
|
4
|
+
*
|
|
5
|
+
* 统一导出技术栈探测、入口文件探测和依赖关系探测的所有公开接口。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
type TechStackResult,
|
|
10
|
+
detectTechStack,
|
|
11
|
+
} from './tech-stack-detector.js';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
detectEntrypoints,
|
|
15
|
+
} from './entrypoint-detector.js';
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
type DependencyEdge,
|
|
19
|
+
type DependencyGraph,
|
|
20
|
+
buildDependencyGraph,
|
|
21
|
+
} from './dependency-detector.js';
|