archtracker-mcp 0.1.1 → 0.2.1
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/cli/index.js +1017 -98
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +1017 -98
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1048 -123
- package/dist/mcp/index.js.map +1 -1
- package/package.json +12 -2
package/dist/cli/index.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { watch } from "fs";
|
|
6
6
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join5 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/analyzer/analyze.ts
|
|
10
|
+
import { resolve as resolve4 } from "path";
|
|
11
|
+
|
|
12
|
+
// src/analyzer/engines/dependency-cruiser.ts
|
|
10
13
|
import { resolve } from "path";
|
|
11
14
|
import { cruise } from "dependency-cruiser";
|
|
12
15
|
var DEFAULT_EXCLUDE = [
|
|
@@ -17,110 +20,1026 @@ var DEFAULT_EXCLUDE = [
|
|
|
17
20
|
"coverage",
|
|
18
21
|
"\\.archtracker"
|
|
19
22
|
];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
23
|
+
var DependencyCruiserEngine = class {
|
|
24
|
+
async analyze(rootDir, options = {}) {
|
|
25
|
+
const {
|
|
26
|
+
exclude = [],
|
|
27
|
+
maxDepth = 0,
|
|
28
|
+
tsConfigPath,
|
|
29
|
+
includeTypeOnly = true
|
|
30
|
+
} = options;
|
|
31
|
+
const absRootDir = resolve(rootDir);
|
|
32
|
+
const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
|
|
33
|
+
const excludePattern = allExclude.join("|");
|
|
34
|
+
const cruiseOptions = {
|
|
35
|
+
baseDir: absRootDir,
|
|
36
|
+
exclude: { path: excludePattern },
|
|
37
|
+
doNotFollow: { path: "node_modules" },
|
|
38
|
+
maxDepth,
|
|
39
|
+
tsPreCompilationDeps: includeTypeOnly ? true : false,
|
|
40
|
+
combinedDependencies: false
|
|
41
|
+
};
|
|
42
|
+
if (tsConfigPath) {
|
|
43
|
+
cruiseOptions.tsConfig = { fileName: tsConfigPath };
|
|
44
|
+
}
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
result = await cruise(["."], cruiseOptions);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new Error(`dependency-cruiser failed: ${message}`, {
|
|
51
|
+
cause: error
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (result.exitCode !== 0 && !result.output) {
|
|
55
|
+
throw new Error(`Analysis exited with code ${result.exitCode}`);
|
|
56
|
+
}
|
|
57
|
+
const cruiseResult = result.output;
|
|
58
|
+
return this.buildGraph(absRootDir, cruiseResult);
|
|
40
59
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
60
|
+
buildGraph(rootDir, cruiseResult) {
|
|
61
|
+
const files = {};
|
|
62
|
+
const edges = [];
|
|
63
|
+
const circularSet = /* @__PURE__ */ new Set();
|
|
64
|
+
const circularDependencies = [];
|
|
65
|
+
for (const mod of cruiseResult.modules) {
|
|
66
|
+
if (this.isExternalModule(mod)) continue;
|
|
67
|
+
files[mod.source] = {
|
|
68
|
+
path: mod.source,
|
|
69
|
+
exists: !mod.couldNotResolve,
|
|
70
|
+
dependencies: [],
|
|
71
|
+
dependents: []
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
for (const mod of cruiseResult.modules) {
|
|
75
|
+
for (const dep of mod.dependencies) {
|
|
76
|
+
if (dep.couldNotResolve || dep.coreModule) continue;
|
|
77
|
+
if (!files[mod.source] || this.isExternalDep(dep)) continue;
|
|
78
|
+
const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
|
|
79
|
+
edges.push({ source: mod.source, target: dep.resolved, type: edgeType });
|
|
80
|
+
if (files[mod.source]) {
|
|
81
|
+
files[mod.source].dependencies.push(dep.resolved);
|
|
82
|
+
}
|
|
83
|
+
if (files[dep.resolved]) {
|
|
84
|
+
files[dep.resolved].dependents.push(mod.source);
|
|
85
|
+
}
|
|
86
|
+
if (dep.circular && dep.cycle) {
|
|
87
|
+
const cyclePath = dep.cycle.map((c) => c.name);
|
|
88
|
+
const cycleKey = [...cyclePath].sort().join("\u2192");
|
|
89
|
+
if (!circularSet.has(cycleKey)) {
|
|
90
|
+
circularSet.add(cycleKey);
|
|
91
|
+
circularDependencies.push({ cycle: cyclePath });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
rootDir,
|
|
98
|
+
files,
|
|
99
|
+
edges,
|
|
100
|
+
circularDependencies,
|
|
101
|
+
totalFiles: Object.keys(files).length,
|
|
102
|
+
totalEdges: edges.length
|
|
103
|
+
};
|
|
50
104
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
105
|
+
isExternalModule(mod) {
|
|
106
|
+
if (mod.coreModule) return true;
|
|
107
|
+
const depTypes = mod.dependencyTypes ?? [];
|
|
108
|
+
if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
|
|
109
|
+
return isExternalPath(mod.source);
|
|
110
|
+
}
|
|
111
|
+
isExternalDep(dep) {
|
|
112
|
+
if (dep.coreModule) return true;
|
|
113
|
+
if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core"))
|
|
114
|
+
return true;
|
|
115
|
+
return isExternalPath(dep.resolved);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function isExternalPath(source) {
|
|
119
|
+
if (source.startsWith("@")) return true;
|
|
120
|
+
if (!source.includes("/") && !source.includes("\\") && !source.includes("."))
|
|
121
|
+
return true;
|
|
122
|
+
if (source.startsWith("node:")) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/analyzer/engines/regex-engine.ts
|
|
127
|
+
import { readdir, readFile } from "fs/promises";
|
|
128
|
+
import { join, relative, resolve as resolve2 } from "path";
|
|
129
|
+
|
|
130
|
+
// src/analyzer/engines/cycle.ts
|
|
131
|
+
function detectCycles(edges) {
|
|
132
|
+
const adj = /* @__PURE__ */ new Map();
|
|
133
|
+
for (const edge of edges) {
|
|
134
|
+
if (!adj.has(edge.source)) adj.set(edge.source, []);
|
|
135
|
+
adj.get(edge.source).push(edge.target);
|
|
136
|
+
}
|
|
137
|
+
const visited = /* @__PURE__ */ new Set();
|
|
138
|
+
const inStack = /* @__PURE__ */ new Set();
|
|
139
|
+
const cycles = [];
|
|
140
|
+
const cycleKeys = /* @__PURE__ */ new Set();
|
|
141
|
+
function dfs(node, path) {
|
|
142
|
+
if (inStack.has(node)) {
|
|
143
|
+
const cycleStart = path.indexOf(node);
|
|
144
|
+
if (cycleStart !== -1) {
|
|
145
|
+
const cycle = path.slice(cycleStart);
|
|
146
|
+
const key = [...cycle].sort().join("\u2192");
|
|
147
|
+
if (!cycleKeys.has(key)) {
|
|
148
|
+
cycleKeys.add(key);
|
|
149
|
+
cycles.push({ cycle });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (visited.has(node)) return;
|
|
155
|
+
visited.add(node);
|
|
156
|
+
inStack.add(node);
|
|
157
|
+
path.push(node);
|
|
158
|
+
for (const neighbor of adj.get(node) ?? []) {
|
|
159
|
+
dfs(neighbor, path);
|
|
160
|
+
}
|
|
161
|
+
path.pop();
|
|
162
|
+
inStack.delete(node);
|
|
163
|
+
}
|
|
164
|
+
for (const node of adj.keys()) {
|
|
165
|
+
if (!visited.has(node)) {
|
|
166
|
+
dfs(node, []);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return cycles;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/analyzer/engines/strip-comments.ts
|
|
173
|
+
function stripComments(content, style) {
|
|
174
|
+
switch (style) {
|
|
175
|
+
case "c-style":
|
|
176
|
+
return stripCStyle(content);
|
|
177
|
+
case "hash":
|
|
178
|
+
return stripHash(content);
|
|
179
|
+
case "python":
|
|
180
|
+
return stripPython(content);
|
|
181
|
+
case "ruby":
|
|
182
|
+
return stripRuby(content);
|
|
183
|
+
case "php":
|
|
184
|
+
return stripPhp(content);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function stripCStyle(content) {
|
|
188
|
+
let result = "";
|
|
189
|
+
let i = 0;
|
|
190
|
+
while (i < content.length) {
|
|
191
|
+
if (content[i] === "/" && content[i + 1] === "/") {
|
|
192
|
+
while (i < content.length && content[i] !== "\n") {
|
|
193
|
+
result += " ";
|
|
194
|
+
i++;
|
|
195
|
+
}
|
|
196
|
+
} else if (content[i] === "/" && content[i + 1] === "*") {
|
|
197
|
+
result += " ";
|
|
198
|
+
i++;
|
|
199
|
+
result += " ";
|
|
200
|
+
i++;
|
|
201
|
+
while (i < content.length) {
|
|
202
|
+
if (content[i] === "*" && content[i + 1] === "/") {
|
|
203
|
+
result += " ";
|
|
204
|
+
i++;
|
|
205
|
+
result += " ";
|
|
206
|
+
i++;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
210
|
+
i++;
|
|
211
|
+
}
|
|
212
|
+
} else if (content[i] === '"') {
|
|
213
|
+
result += content[i];
|
|
214
|
+
i++;
|
|
215
|
+
while (i < content.length && content[i] !== '"') {
|
|
216
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
217
|
+
result += content[i];
|
|
218
|
+
i++;
|
|
219
|
+
result += content[i];
|
|
220
|
+
i++;
|
|
221
|
+
} else if (content[i] === "\n") {
|
|
222
|
+
result += "\n";
|
|
223
|
+
i++;
|
|
224
|
+
} else {
|
|
225
|
+
result += content[i];
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (i < content.length) {
|
|
230
|
+
result += content[i];
|
|
231
|
+
i++;
|
|
232
|
+
}
|
|
233
|
+
} else if (content[i] === "`") {
|
|
234
|
+
result += " ";
|
|
235
|
+
i++;
|
|
236
|
+
while (i < content.length && content[i] !== "`") {
|
|
237
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
238
|
+
i++;
|
|
239
|
+
}
|
|
240
|
+
if (i < content.length) {
|
|
241
|
+
result += " ";
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
result += content[i];
|
|
246
|
+
i++;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function stripHash(content) {
|
|
252
|
+
let result = "";
|
|
253
|
+
let i = 0;
|
|
254
|
+
while (i < content.length) {
|
|
255
|
+
if (content[i] === "#") {
|
|
256
|
+
while (i < content.length && content[i] !== "\n") {
|
|
257
|
+
result += " ";
|
|
258
|
+
i++;
|
|
259
|
+
}
|
|
260
|
+
} else if (content[i] === '"') {
|
|
261
|
+
result += content[i];
|
|
262
|
+
i++;
|
|
263
|
+
while (i < content.length && content[i] !== '"') {
|
|
264
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
265
|
+
result += content[i++];
|
|
266
|
+
result += content[i++];
|
|
267
|
+
} else {
|
|
268
|
+
result += content[i++];
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (i < content.length) {
|
|
272
|
+
result += content[i];
|
|
273
|
+
i++;
|
|
274
|
+
}
|
|
275
|
+
} else if (content[i] === "'") {
|
|
276
|
+
result += content[i];
|
|
277
|
+
i++;
|
|
278
|
+
while (i < content.length && content[i] !== "'") {
|
|
279
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
280
|
+
result += content[i++];
|
|
281
|
+
result += content[i++];
|
|
282
|
+
} else {
|
|
283
|
+
result += content[i++];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (i < content.length) {
|
|
287
|
+
result += content[i];
|
|
288
|
+
i++;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
result += content[i];
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
function stripPython(content) {
|
|
298
|
+
let result = "";
|
|
299
|
+
let i = 0;
|
|
300
|
+
while (i < content.length) {
|
|
301
|
+
if (content[i] === '"' && content[i + 1] === '"' && content[i + 2] === '"' || content[i] === "'" && content[i + 1] === "'" && content[i + 2] === "'") {
|
|
302
|
+
const quote = content[i];
|
|
303
|
+
const tripleQuote = quote + quote + quote;
|
|
304
|
+
result += " ";
|
|
305
|
+
i += 3;
|
|
306
|
+
while (i < content.length) {
|
|
307
|
+
if (content[i] === quote && content[i + 1] === quote && content[i + 2] === quote) {
|
|
308
|
+
result += " ";
|
|
309
|
+
i += 3;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
} else if (content[i] === "#") {
|
|
316
|
+
while (i < content.length && content[i] !== "\n") {
|
|
317
|
+
result += " ";
|
|
318
|
+
i++;
|
|
319
|
+
}
|
|
320
|
+
} else if (content[i] === '"' || content[i] === "'") {
|
|
321
|
+
const quote = content[i];
|
|
322
|
+
result += content[i];
|
|
323
|
+
i++;
|
|
324
|
+
while (i < content.length && content[i] !== quote) {
|
|
325
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
326
|
+
result += content[i++];
|
|
327
|
+
result += content[i++];
|
|
328
|
+
} else if (content[i] === "\n") {
|
|
329
|
+
result += "\n";
|
|
330
|
+
i++;
|
|
331
|
+
break;
|
|
332
|
+
} else {
|
|
333
|
+
result += content[i++];
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (i < content.length && content[i] === quote) {
|
|
337
|
+
result += content[i];
|
|
338
|
+
i++;
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
result += content[i];
|
|
342
|
+
i++;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
function stripRuby(content) {
|
|
348
|
+
const lines = content.split("\n");
|
|
349
|
+
const result = [];
|
|
350
|
+
let inBlock = false;
|
|
351
|
+
for (const line of lines) {
|
|
352
|
+
if (!inBlock && line.startsWith("=begin")) {
|
|
353
|
+
inBlock = true;
|
|
354
|
+
result.push(" ".repeat(line.length));
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (inBlock) {
|
|
358
|
+
if (line.startsWith("=end")) {
|
|
359
|
+
inBlock = false;
|
|
360
|
+
}
|
|
361
|
+
result.push(" ".repeat(line.length));
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
let processed = "";
|
|
365
|
+
let i = 0;
|
|
366
|
+
while (i < line.length) {
|
|
367
|
+
if (line[i] === "#") {
|
|
368
|
+
processed += " ".repeat(line.length - i);
|
|
369
|
+
break;
|
|
370
|
+
} else if (line[i] === '"') {
|
|
371
|
+
processed += line[i];
|
|
372
|
+
i++;
|
|
373
|
+
while (i < line.length && line[i] !== '"') {
|
|
374
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
375
|
+
processed += line[i++];
|
|
376
|
+
processed += line[i++];
|
|
377
|
+
} else {
|
|
378
|
+
processed += line[i++];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (i < line.length) {
|
|
382
|
+
processed += line[i];
|
|
383
|
+
i++;
|
|
384
|
+
}
|
|
385
|
+
} else if (line[i] === "'") {
|
|
386
|
+
processed += line[i];
|
|
387
|
+
i++;
|
|
388
|
+
while (i < line.length && line[i] !== "'") {
|
|
389
|
+
if (line[i] === "\\" && i + 1 < line.length) {
|
|
390
|
+
processed += line[i++];
|
|
391
|
+
processed += line[i++];
|
|
392
|
+
} else {
|
|
393
|
+
processed += line[i++];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (i < line.length) {
|
|
397
|
+
processed += line[i];
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
processed += line[i];
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
result.push(processed);
|
|
406
|
+
}
|
|
407
|
+
return result.join("\n");
|
|
408
|
+
}
|
|
409
|
+
function stripPhp(content) {
|
|
410
|
+
let result = "";
|
|
411
|
+
let i = 0;
|
|
412
|
+
while (i < content.length) {
|
|
413
|
+
if (content[i] === "/" && content[i + 1] === "/" || content[i] === "#") {
|
|
414
|
+
while (i < content.length && content[i] !== "\n") {
|
|
415
|
+
result += " ";
|
|
416
|
+
i++;
|
|
417
|
+
}
|
|
418
|
+
} else if (content[i] === "/" && content[i + 1] === "*") {
|
|
419
|
+
result += " ";
|
|
420
|
+
i++;
|
|
421
|
+
result += " ";
|
|
422
|
+
i++;
|
|
423
|
+
while (i < content.length) {
|
|
424
|
+
if (content[i] === "*" && content[i + 1] === "/") {
|
|
425
|
+
result += " ";
|
|
426
|
+
i++;
|
|
427
|
+
result += " ";
|
|
428
|
+
i++;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
result += content[i] === "\n" ? "\n" : " ";
|
|
432
|
+
i++;
|
|
433
|
+
}
|
|
434
|
+
} else if (content[i] === '"') {
|
|
435
|
+
result += content[i];
|
|
436
|
+
i++;
|
|
437
|
+
while (i < content.length && content[i] !== '"') {
|
|
438
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
439
|
+
result += content[i++];
|
|
440
|
+
result += content[i++];
|
|
441
|
+
} else {
|
|
442
|
+
result += content[i++];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (i < content.length) {
|
|
446
|
+
result += content[i];
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
} else if (content[i] === "'") {
|
|
450
|
+
result += content[i];
|
|
451
|
+
i++;
|
|
452
|
+
while (i < content.length && content[i] !== "'") {
|
|
453
|
+
if (content[i] === "\\" && i + 1 < content.length) {
|
|
454
|
+
result += content[i++];
|
|
455
|
+
result += content[i++];
|
|
456
|
+
} else {
|
|
457
|
+
result += content[i++];
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (i < content.length) {
|
|
461
|
+
result += content[i];
|
|
462
|
+
i++;
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
result += content[i];
|
|
466
|
+
i++;
|
|
467
|
+
}
|
|
55
468
|
}
|
|
56
|
-
|
|
57
|
-
return buildGraph(absRootDir, cruiseResult);
|
|
469
|
+
return result;
|
|
58
470
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
471
|
+
|
|
472
|
+
// src/analyzer/engines/regex-engine.ts
|
|
473
|
+
var RegexEngine = class {
|
|
474
|
+
constructor(config) {
|
|
475
|
+
this.config = config;
|
|
476
|
+
}
|
|
477
|
+
async analyze(rootDir, options = {}) {
|
|
478
|
+
const absRootDir = resolve2(rootDir);
|
|
479
|
+
const excludePatterns = [
|
|
480
|
+
...this.config.defaultExclude ?? [],
|
|
481
|
+
...options.exclude ?? [],
|
|
482
|
+
"\\.archtracker"
|
|
483
|
+
].map((p) => new RegExp(p));
|
|
484
|
+
const projectFiles = await this.collectFiles(
|
|
485
|
+
absRootDir,
|
|
486
|
+
excludePatterns,
|
|
487
|
+
options.maxDepth ?? 0
|
|
488
|
+
);
|
|
489
|
+
const projectFileSet = new Set(projectFiles);
|
|
490
|
+
const files = {};
|
|
491
|
+
const edges = [];
|
|
492
|
+
const edgeSet = /* @__PURE__ */ new Set();
|
|
493
|
+
for (const filePath of projectFiles) {
|
|
494
|
+
const relPath = relative(absRootDir, filePath);
|
|
495
|
+
files[relPath] = {
|
|
496
|
+
path: relPath,
|
|
497
|
+
exists: true,
|
|
498
|
+
dependencies: [],
|
|
499
|
+
dependents: []
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
for (const filePath of projectFiles) {
|
|
503
|
+
const relSource = relative(absRootDir, filePath);
|
|
504
|
+
let content;
|
|
505
|
+
try {
|
|
506
|
+
content = await readFile(filePath, "utf-8");
|
|
507
|
+
} catch {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
const stripped = stripComments(content, this.config.commentStyle);
|
|
511
|
+
const imports = this.extractImports(stripped);
|
|
512
|
+
for (const importPath of imports) {
|
|
513
|
+
const resolved = this.config.resolveImport(
|
|
514
|
+
importPath,
|
|
515
|
+
filePath,
|
|
516
|
+
absRootDir,
|
|
517
|
+
projectFileSet
|
|
518
|
+
);
|
|
519
|
+
if (!resolved) continue;
|
|
520
|
+
const relTarget = relative(absRootDir, resolved);
|
|
521
|
+
if (!files[relTarget]) continue;
|
|
522
|
+
if (relSource === relTarget) continue;
|
|
523
|
+
const edgeKey = `${relSource}\0${relTarget}`;
|
|
524
|
+
if (edgeSet.has(edgeKey)) continue;
|
|
525
|
+
edgeSet.add(edgeKey);
|
|
526
|
+
edges.push({
|
|
527
|
+
source: relSource,
|
|
528
|
+
target: relTarget,
|
|
529
|
+
type: "static"
|
|
530
|
+
});
|
|
531
|
+
files[relSource].dependencies.push(relTarget);
|
|
532
|
+
files[relTarget].dependents.push(relSource);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const circularDependencies = detectCycles(edges);
|
|
536
|
+
return {
|
|
537
|
+
rootDir: absRootDir,
|
|
538
|
+
files,
|
|
539
|
+
edges,
|
|
540
|
+
circularDependencies,
|
|
541
|
+
totalFiles: Object.keys(files).length,
|
|
542
|
+
totalEdges: edges.length
|
|
71
543
|
};
|
|
72
544
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
545
|
+
extractImports(content) {
|
|
546
|
+
if (this.config.extractImports) {
|
|
547
|
+
return this.config.extractImports(content);
|
|
548
|
+
}
|
|
549
|
+
const imports = [];
|
|
550
|
+
for (const pattern of this.config.importPatterns) {
|
|
551
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
552
|
+
let match;
|
|
553
|
+
while ((match = regex.exec(content)) !== null) {
|
|
554
|
+
if (match[1]) {
|
|
555
|
+
imports.push(match[1]);
|
|
556
|
+
}
|
|
85
557
|
}
|
|
86
|
-
|
|
87
|
-
|
|
558
|
+
}
|
|
559
|
+
return imports;
|
|
560
|
+
}
|
|
561
|
+
async collectFiles(dir, excludePatterns, maxDepth, currentDepth = 0) {
|
|
562
|
+
if (maxDepth > 0 && currentDepth >= maxDepth) return [];
|
|
563
|
+
const results = [];
|
|
564
|
+
let entries;
|
|
565
|
+
try {
|
|
566
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
567
|
+
} catch {
|
|
568
|
+
return results;
|
|
569
|
+
}
|
|
570
|
+
for (const entry of entries) {
|
|
571
|
+
const fullPath = join(dir, entry.name);
|
|
572
|
+
const relPath = relative(dir, fullPath);
|
|
573
|
+
if (excludePatterns.some(
|
|
574
|
+
(p) => p.test(entry.name) || p.test(relPath) || p.test(fullPath)
|
|
575
|
+
)) {
|
|
576
|
+
continue;
|
|
88
577
|
}
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
578
|
+
if (entry.isDirectory()) {
|
|
579
|
+
if (entry.name.startsWith(".")) continue;
|
|
580
|
+
const sub = await this.collectFiles(
|
|
581
|
+
fullPath,
|
|
582
|
+
excludePatterns,
|
|
583
|
+
maxDepth,
|
|
584
|
+
currentDepth + 1
|
|
585
|
+
);
|
|
586
|
+
results.push(...sub);
|
|
587
|
+
} else if (entry.isFile()) {
|
|
588
|
+
const dotIdx = entry.name.lastIndexOf(".");
|
|
589
|
+
if (dotIdx > 0) {
|
|
590
|
+
const ext = entry.name.slice(dotIdx);
|
|
591
|
+
if (this.config.extensions.includes(ext)) {
|
|
592
|
+
results.push(fullPath);
|
|
593
|
+
}
|
|
95
594
|
}
|
|
96
595
|
}
|
|
97
596
|
}
|
|
597
|
+
return results;
|
|
98
598
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/analyzer/engines/detect.ts
|
|
602
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
603
|
+
import { join as join2 } from "path";
|
|
604
|
+
var MARKERS = [
|
|
605
|
+
{ file: "Cargo.toml", language: "rust" },
|
|
606
|
+
{ file: "go.mod", language: "go" },
|
|
607
|
+
{ file: "pyproject.toml", language: "python" },
|
|
608
|
+
{ file: "setup.py", language: "python" },
|
|
609
|
+
{ file: "requirements.txt", language: "python" },
|
|
610
|
+
{ file: "Pipfile", language: "python" },
|
|
611
|
+
{ file: "pom.xml", language: "java" },
|
|
612
|
+
{ file: "build.gradle", language: "java" },
|
|
613
|
+
{ file: "build.gradle.kts", language: "kotlin" },
|
|
614
|
+
{ file: "Package.swift", language: "swift" },
|
|
615
|
+
{ file: "Gemfile", language: "ruby" },
|
|
616
|
+
{ file: "composer.json", language: "php" },
|
|
617
|
+
{ file: "CMakeLists.txt", language: "c-cpp" },
|
|
618
|
+
{ file: "Makefile", language: "c-cpp" },
|
|
619
|
+
{ file: "package.json", language: "javascript" },
|
|
620
|
+
{ file: "tsconfig.json", language: "javascript" }
|
|
621
|
+
];
|
|
622
|
+
var EXT_MAP = {
|
|
623
|
+
".ts": "javascript",
|
|
624
|
+
".tsx": "javascript",
|
|
625
|
+
".js": "javascript",
|
|
626
|
+
".jsx": "javascript",
|
|
627
|
+
".mjs": "javascript",
|
|
628
|
+
".cjs": "javascript",
|
|
629
|
+
".py": "python",
|
|
630
|
+
".rs": "rust",
|
|
631
|
+
".go": "go",
|
|
632
|
+
".java": "java",
|
|
633
|
+
".c": "c-cpp",
|
|
634
|
+
".cpp": "c-cpp",
|
|
635
|
+
".cc": "c-cpp",
|
|
636
|
+
".cxx": "c-cpp",
|
|
637
|
+
".h": "c-cpp",
|
|
638
|
+
".hpp": "c-cpp",
|
|
639
|
+
".rb": "ruby",
|
|
640
|
+
".php": "php",
|
|
641
|
+
".swift": "swift",
|
|
642
|
+
".kt": "kotlin",
|
|
643
|
+
".kts": "kotlin"
|
|
644
|
+
};
|
|
645
|
+
async function detectLanguage(rootDir) {
|
|
646
|
+
for (const marker of MARKERS) {
|
|
647
|
+
try {
|
|
648
|
+
const s = await stat2(join2(rootDir, marker.file));
|
|
649
|
+
if (s.isFile() || s.isDirectory()) {
|
|
650
|
+
return marker.language;
|
|
651
|
+
}
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const counts = /* @__PURE__ */ new Map();
|
|
656
|
+
try {
|
|
657
|
+
await scanExtensions(rootDir, counts, 2, 0);
|
|
658
|
+
} catch {
|
|
659
|
+
}
|
|
660
|
+
if (counts.size > 0) {
|
|
661
|
+
let maxLang = "javascript";
|
|
662
|
+
let maxCount = 0;
|
|
663
|
+
for (const [lang, count] of counts) {
|
|
664
|
+
if (count > maxCount) {
|
|
665
|
+
maxCount = count;
|
|
666
|
+
maxLang = lang;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return maxLang;
|
|
670
|
+
}
|
|
671
|
+
return "javascript";
|
|
107
672
|
}
|
|
108
|
-
function
|
|
109
|
-
if (
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
673
|
+
async function scanExtensions(dir, counts, maxDepth, currentDepth) {
|
|
674
|
+
if (currentDepth >= maxDepth) return;
|
|
675
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
676
|
+
for (const entry of entries) {
|
|
677
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
678
|
+
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
679
|
+
await scanExtensions(
|
|
680
|
+
join2(dir, entry.name),
|
|
681
|
+
counts,
|
|
682
|
+
maxDepth,
|
|
683
|
+
currentDepth + 1
|
|
684
|
+
);
|
|
685
|
+
} else if (entry.isFile()) {
|
|
686
|
+
const dotIdx = entry.name.lastIndexOf(".");
|
|
687
|
+
if (dotIdx > 0) {
|
|
688
|
+
const ext = entry.name.slice(dotIdx);
|
|
689
|
+
const lang = EXT_MAP[ext];
|
|
690
|
+
if (lang) {
|
|
691
|
+
counts.set(lang, (counts.get(lang) ?? 0) + 1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
113
696
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
697
|
+
|
|
698
|
+
// src/analyzer/engines/languages.ts
|
|
699
|
+
import { readFileSync } from "fs";
|
|
700
|
+
import { join as join3, dirname, resolve as resolve3 } from "path";
|
|
701
|
+
var python = {
|
|
702
|
+
id: "python",
|
|
703
|
+
extensions: [".py"],
|
|
704
|
+
commentStyle: "python",
|
|
705
|
+
importPatterns: [
|
|
706
|
+
// from package.module import something
|
|
707
|
+
{ regex: /^from\s+(\.[\w.]*|\w[\w.]*)\s+import\b/gm },
|
|
708
|
+
// import package.module
|
|
709
|
+
{ regex: /^import\s+([\w.]+)/gm }
|
|
710
|
+
],
|
|
711
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
712
|
+
if (importPath.startsWith(".")) {
|
|
713
|
+
const dots = importPath.match(/^\.+/)?.[0].length ?? 1;
|
|
714
|
+
let base = dirname(sourceFile);
|
|
715
|
+
for (let i = 1; i < dots; i++) base = dirname(base);
|
|
716
|
+
const rest = importPath.slice(dots).replace(/\./g, "/");
|
|
717
|
+
return tryPythonResolve(join3(base, rest), projectFiles);
|
|
718
|
+
}
|
|
719
|
+
const parts = importPath.replace(/\./g, "/");
|
|
720
|
+
return tryPythonResolve(join3(rootDir, parts), projectFiles);
|
|
721
|
+
},
|
|
722
|
+
defaultExclude: ["__pycache__", "\\.venv", "venv", "\\.egg-info", "dist", "build"]
|
|
723
|
+
};
|
|
724
|
+
function tryPythonResolve(base, projectFiles) {
|
|
725
|
+
if (projectFiles.has(base + ".py")) return base + ".py";
|
|
726
|
+
if (projectFiles.has(join3(base, "__init__.py"))) return join3(base, "__init__.py");
|
|
727
|
+
return null;
|
|
118
728
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
729
|
+
var rust = {
|
|
730
|
+
id: "rust",
|
|
731
|
+
extensions: [".rs"],
|
|
732
|
+
commentStyle: "c-style",
|
|
733
|
+
importPatterns: [],
|
|
734
|
+
// handled by extractImports
|
|
735
|
+
extractImports(content) {
|
|
736
|
+
const imports = [];
|
|
737
|
+
const modRegex = /\bmod\s+(\w+)\s*;/gm;
|
|
738
|
+
let match;
|
|
739
|
+
while ((match = modRegex.exec(content)) !== null) {
|
|
740
|
+
imports.push(match[1]);
|
|
741
|
+
}
|
|
742
|
+
const useRegex = /\buse\s+crate::([\s\S]*?);/gm;
|
|
743
|
+
while ((match = useRegex.exec(content)) !== null) {
|
|
744
|
+
const body = match[1].trim();
|
|
745
|
+
extractRustUsePaths(body, "", imports);
|
|
746
|
+
}
|
|
747
|
+
return imports;
|
|
748
|
+
},
|
|
749
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
750
|
+
const srcDir = join3(rootDir, "src");
|
|
751
|
+
if (!importPath.includes("::")) {
|
|
752
|
+
const parentDir = dirname(sourceFile);
|
|
753
|
+
const asFile = join3(parentDir, importPath + ".rs");
|
|
754
|
+
if (projectFiles.has(asFile)) return asFile;
|
|
755
|
+
const asDir = join3(parentDir, importPath, "mod.rs");
|
|
756
|
+
if (projectFiles.has(asDir)) return asDir;
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
const segments = importPath.split("::");
|
|
760
|
+
for (let i = segments.length; i > 0; i--) {
|
|
761
|
+
const path = segments.slice(0, i).join("/");
|
|
762
|
+
const asFile = join3(srcDir, path + ".rs");
|
|
763
|
+
if (projectFiles.has(asFile)) return asFile;
|
|
764
|
+
const asDir = join3(srcDir, path, "mod.rs");
|
|
765
|
+
if (projectFiles.has(asDir)) return asDir;
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
},
|
|
769
|
+
defaultExclude: ["target"]
|
|
770
|
+
};
|
|
771
|
+
function extractRustUsePaths(body, prefix, results) {
|
|
772
|
+
const trimmed = body.trim();
|
|
773
|
+
const braceStart = trimmed.indexOf("{");
|
|
774
|
+
if (braceStart === -1) {
|
|
775
|
+
const path = prefix ? `${prefix}::${trimmed}` : trimmed;
|
|
776
|
+
if (path && !path.includes("{")) {
|
|
777
|
+
results.push(path);
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
let pathPrefix = trimmed.slice(0, braceStart).trim();
|
|
782
|
+
if (pathPrefix.endsWith("::")) {
|
|
783
|
+
pathPrefix = pathPrefix.slice(0, -2);
|
|
784
|
+
}
|
|
785
|
+
const fullPrefix = prefix ? `${prefix}::${pathPrefix}` : pathPrefix;
|
|
786
|
+
let depth = 0;
|
|
787
|
+
let braceEnd = -1;
|
|
788
|
+
for (let i = braceStart; i < trimmed.length; i++) {
|
|
789
|
+
if (trimmed[i] === "{") depth++;
|
|
790
|
+
else if (trimmed[i] === "}") {
|
|
791
|
+
depth--;
|
|
792
|
+
if (depth === 0) {
|
|
793
|
+
braceEnd = i;
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (braceEnd === -1) return;
|
|
799
|
+
const inner = trimmed.slice(braceStart + 1, braceEnd).trim();
|
|
800
|
+
const items = splitByTopLevelComma(inner);
|
|
801
|
+
for (const item of items) {
|
|
802
|
+
const cleaned = item.trim();
|
|
803
|
+
if (cleaned === "self") {
|
|
804
|
+
results.push(fullPrefix);
|
|
805
|
+
} else if (cleaned) {
|
|
806
|
+
extractRustUsePaths(cleaned, fullPrefix, results);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
function splitByTopLevelComma(s) {
|
|
811
|
+
const parts = [];
|
|
812
|
+
let depth = 0;
|
|
813
|
+
let start = 0;
|
|
814
|
+
for (let i = 0; i < s.length; i++) {
|
|
815
|
+
if (s[i] === "{") depth++;
|
|
816
|
+
else if (s[i] === "}") depth--;
|
|
817
|
+
else if (s[i] === "," && depth === 0) {
|
|
818
|
+
parts.push(s.slice(start, i));
|
|
819
|
+
start = i + 1;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
parts.push(s.slice(start));
|
|
823
|
+
return parts;
|
|
824
|
+
}
|
|
825
|
+
var go = {
|
|
826
|
+
id: "go",
|
|
827
|
+
extensions: [".go"],
|
|
828
|
+
commentStyle: "c-style",
|
|
829
|
+
importPatterns: [],
|
|
830
|
+
// handled by extractImports
|
|
831
|
+
extractImports(content) {
|
|
832
|
+
const imports = [];
|
|
833
|
+
const singleRegex = /\bimport\s+(?:\w+\s+)?"([^"]+)"/gm;
|
|
834
|
+
let match;
|
|
835
|
+
while ((match = singleRegex.exec(content)) !== null) {
|
|
836
|
+
imports.push(match[1]);
|
|
837
|
+
}
|
|
838
|
+
const blockRegex = /\bimport\s*\(([^)]*)\)/gms;
|
|
839
|
+
while ((match = blockRegex.exec(content)) !== null) {
|
|
840
|
+
const block = match[1];
|
|
841
|
+
const entryRegex = /(?:\w+\s+)?"([^"]+)"/g;
|
|
842
|
+
let entry;
|
|
843
|
+
while ((entry = entryRegex.exec(block)) !== null) {
|
|
844
|
+
imports.push(entry[1]);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return imports;
|
|
848
|
+
},
|
|
849
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
850
|
+
const modPrefix = goModulePrefix(rootDir);
|
|
851
|
+
if (!modPrefix || !importPath.startsWith(modPrefix)) return null;
|
|
852
|
+
const relPath = importPath.slice(modPrefix.length + 1);
|
|
853
|
+
const pkgDir = join3(rootDir, relPath);
|
|
854
|
+
for (const f of projectFiles) {
|
|
855
|
+
if (f.startsWith(pkgDir + "/") && f.endsWith(".go")) return f;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
},
|
|
859
|
+
defaultExclude: ["vendor"]
|
|
860
|
+
};
|
|
861
|
+
var goModCache = /* @__PURE__ */ new Map();
|
|
862
|
+
function goModulePrefix(rootDir) {
|
|
863
|
+
if (goModCache.has(rootDir)) return goModCache.get(rootDir);
|
|
864
|
+
try {
|
|
865
|
+
const content = readFileSync(join3(rootDir, "go.mod"), "utf-8");
|
|
866
|
+
const match = content.match(/^module\s+(.+)$/m);
|
|
867
|
+
const prefix = match ? match[1].trim() : null;
|
|
868
|
+
goModCache.set(rootDir, prefix);
|
|
869
|
+
return prefix;
|
|
870
|
+
} catch {
|
|
871
|
+
goModCache.set(rootDir, null);
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
var java = {
|
|
876
|
+
id: "java",
|
|
877
|
+
extensions: [".java"],
|
|
878
|
+
commentStyle: "c-style",
|
|
879
|
+
importPatterns: [
|
|
880
|
+
// import com.example.ClassName;
|
|
881
|
+
{ regex: /^import\s+(?:static\s+)?([\w.]+);/gm }
|
|
882
|
+
],
|
|
883
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
884
|
+
const filePath = importPath.replace(/\./g, "/") + ".java";
|
|
885
|
+
for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
|
|
886
|
+
const full = join3(rootDir, srcRoot, filePath);
|
|
887
|
+
if (projectFiles.has(full)) return full;
|
|
888
|
+
}
|
|
889
|
+
return null;
|
|
890
|
+
},
|
|
891
|
+
defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
|
|
892
|
+
};
|
|
893
|
+
var cCpp = {
|
|
894
|
+
id: "c-cpp",
|
|
895
|
+
extensions: [".c", ".cpp", ".cc", ".cxx", ".h", ".hpp"],
|
|
896
|
+
commentStyle: "c-style",
|
|
897
|
+
importPatterns: [
|
|
898
|
+
// #include "file.h" (skip <system> includes)
|
|
899
|
+
{ regex: /^#include\s+"([^"]+)"/gm }
|
|
900
|
+
],
|
|
901
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
902
|
+
const fromSource = resolve3(dirname(sourceFile), importPath);
|
|
903
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
904
|
+
const fromRoot = join3(rootDir, importPath);
|
|
905
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
906
|
+
for (const incDir of ["include", "src"]) {
|
|
907
|
+
const full = join3(rootDir, incDir, importPath);
|
|
908
|
+
if (projectFiles.has(full)) return full;
|
|
909
|
+
}
|
|
910
|
+
return null;
|
|
911
|
+
},
|
|
912
|
+
defaultExclude: ["build", "cmake-build", "\\.o$", "\\.obj$"]
|
|
913
|
+
};
|
|
914
|
+
var ruby = {
|
|
915
|
+
id: "ruby",
|
|
916
|
+
extensions: [".rb"],
|
|
917
|
+
commentStyle: "ruby",
|
|
918
|
+
importPatterns: [
|
|
919
|
+
// require_relative 'path'
|
|
920
|
+
{ regex: /\brequire_relative\s+['"]([^'"]+)['"]/gm },
|
|
921
|
+
// require 'path' (for project-internal requires)
|
|
922
|
+
{ regex: /\brequire\s+['"]([^'"]+)['"]/gm }
|
|
923
|
+
],
|
|
924
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
925
|
+
const withExt = importPath.endsWith(".rb") ? importPath : importPath + ".rb";
|
|
926
|
+
const fromSource = resolve3(dirname(sourceFile), withExt);
|
|
927
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
928
|
+
const fromRoot = join3(rootDir, withExt);
|
|
929
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
930
|
+
const fromLib = join3(rootDir, "lib", withExt);
|
|
931
|
+
if (projectFiles.has(fromLib)) return fromLib;
|
|
932
|
+
return null;
|
|
933
|
+
},
|
|
934
|
+
defaultExclude: ["vendor", "\\.bundle"]
|
|
935
|
+
};
|
|
936
|
+
var php = {
|
|
937
|
+
id: "php",
|
|
938
|
+
extensions: [".php"],
|
|
939
|
+
commentStyle: "php",
|
|
940
|
+
importPatterns: [
|
|
941
|
+
// require/include/require_once/include_once 'path'
|
|
942
|
+
{ regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
|
|
943
|
+
// use Namespace\Class (PSR-4 style)
|
|
944
|
+
{ regex: /^use\s+([\w\\]+)/gm }
|
|
945
|
+
],
|
|
946
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
947
|
+
if (importPath.includes("/") || importPath.endsWith(".php")) {
|
|
948
|
+
const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
|
|
949
|
+
const fromSource = resolve3(dirname(sourceFile), withExt);
|
|
950
|
+
if (projectFiles.has(fromSource)) return fromSource;
|
|
951
|
+
const fromRoot2 = join3(rootDir, withExt);
|
|
952
|
+
if (projectFiles.has(fromRoot2)) return fromRoot2;
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
const filePath = importPath.replace(/\\/g, "/") + ".php";
|
|
956
|
+
const fromRoot = join3(rootDir, filePath);
|
|
957
|
+
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
958
|
+
const fromSrc = join3(rootDir, "src", filePath);
|
|
959
|
+
if (projectFiles.has(fromSrc)) return fromSrc;
|
|
960
|
+
return null;
|
|
961
|
+
},
|
|
962
|
+
defaultExclude: ["vendor"]
|
|
963
|
+
};
|
|
964
|
+
var swift = {
|
|
965
|
+
id: "swift",
|
|
966
|
+
extensions: [".swift"],
|
|
967
|
+
commentStyle: "c-style",
|
|
968
|
+
importPatterns: [
|
|
969
|
+
// import ModuleName (for cross-module dependencies)
|
|
970
|
+
{ regex: /^import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
|
|
971
|
+
],
|
|
972
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
973
|
+
const spmDir = join3(rootDir, "Sources", importPath);
|
|
974
|
+
for (const f of projectFiles) {
|
|
975
|
+
if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
},
|
|
979
|
+
defaultExclude: ["\\.build", "DerivedData"]
|
|
980
|
+
};
|
|
981
|
+
var kotlin = {
|
|
982
|
+
id: "kotlin",
|
|
983
|
+
extensions: [".kt", ".kts"],
|
|
984
|
+
commentStyle: "c-style",
|
|
985
|
+
importPatterns: [
|
|
986
|
+
// import com.example.ClassName
|
|
987
|
+
{ regex: /^import\s+([\w.]+)/gm }
|
|
988
|
+
],
|
|
989
|
+
resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
|
|
990
|
+
const filePath = importPath.replace(/\./g, "/");
|
|
991
|
+
for (const ext of [".kt", ".kts"]) {
|
|
992
|
+
for (const srcRoot of [
|
|
993
|
+
"",
|
|
994
|
+
"src/main/kotlin/",
|
|
995
|
+
"src/main/java/",
|
|
996
|
+
"src/",
|
|
997
|
+
"app/src/main/kotlin/",
|
|
998
|
+
"app/src/main/java/"
|
|
999
|
+
]) {
|
|
1000
|
+
const full = join3(rootDir, srcRoot, filePath + ext);
|
|
1001
|
+
if (projectFiles.has(full)) return full;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return null;
|
|
1005
|
+
},
|
|
1006
|
+
defaultExclude: ["build", "\\.gradle", "\\.idea"]
|
|
1007
|
+
};
|
|
1008
|
+
var LANGUAGE_CONFIGS = {
|
|
1009
|
+
javascript: null,
|
|
1010
|
+
// handled by DependencyCruiserEngine
|
|
1011
|
+
python,
|
|
1012
|
+
rust,
|
|
1013
|
+
go,
|
|
1014
|
+
java,
|
|
1015
|
+
"c-cpp": cCpp,
|
|
1016
|
+
ruby,
|
|
1017
|
+
php,
|
|
1018
|
+
swift,
|
|
1019
|
+
kotlin
|
|
1020
|
+
};
|
|
1021
|
+
function getLanguageConfig(id) {
|
|
1022
|
+
return LANGUAGE_CONFIGS[id] ?? null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/analyzer/analyze.ts
|
|
1026
|
+
async function analyzeProject(rootDir, options = {}) {
|
|
1027
|
+
const absRootDir = resolve4(rootDir);
|
|
1028
|
+
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1029
|
+
try {
|
|
1030
|
+
if (language === "javascript") {
|
|
1031
|
+
return await new DependencyCruiserEngine().analyze(absRootDir, options);
|
|
1032
|
+
}
|
|
1033
|
+
const config = getLanguageConfig(language);
|
|
1034
|
+
if (!config) {
|
|
1035
|
+
throw new AnalyzerError(`No analyzer config for language: ${language}`);
|
|
1036
|
+
}
|
|
1037
|
+
return await new RegexEngine(config).analyze(absRootDir, options);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
if (error instanceof AnalyzerError) throw error;
|
|
1040
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1041
|
+
throw new AnalyzerError(message, { cause: error });
|
|
1042
|
+
}
|
|
124
1043
|
}
|
|
125
1044
|
var AnalyzerError = class extends Error {
|
|
126
1045
|
constructor(message, options) {
|
|
@@ -387,8 +1306,8 @@ function formatAnalysisReport(graph, options = {}) {
|
|
|
387
1306
|
}
|
|
388
1307
|
|
|
389
1308
|
// src/storage/snapshot.ts
|
|
390
|
-
import { mkdir, writeFile, readFile, access } from "fs/promises";
|
|
391
|
-
import { join } from "path";
|
|
1309
|
+
import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
|
|
1310
|
+
import { join as join4 } from "path";
|
|
392
1311
|
import { z } from "zod";
|
|
393
1312
|
|
|
394
1313
|
// src/types/schema.ts
|
|
@@ -422,8 +1341,8 @@ var SnapshotSchema = z.object({
|
|
|
422
1341
|
graph: DependencyGraphSchema
|
|
423
1342
|
});
|
|
424
1343
|
async function saveSnapshot(projectRoot, graph) {
|
|
425
|
-
const dirPath =
|
|
426
|
-
const filePath =
|
|
1344
|
+
const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
|
|
1345
|
+
const filePath = join4(dirPath, SNAPSHOT_FILE);
|
|
427
1346
|
const snapshot = {
|
|
428
1347
|
version: SCHEMA_VERSION,
|
|
429
1348
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -435,10 +1354,10 @@ async function saveSnapshot(projectRoot, graph) {
|
|
|
435
1354
|
return snapshot;
|
|
436
1355
|
}
|
|
437
1356
|
async function loadSnapshot(projectRoot) {
|
|
438
|
-
const filePath =
|
|
1357
|
+
const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
|
|
439
1358
|
let raw;
|
|
440
1359
|
try {
|
|
441
|
-
raw = await
|
|
1360
|
+
raw = await readFile2(filePath, "utf-8");
|
|
442
1361
|
} catch (error) {
|
|
443
1362
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
444
1363
|
return null;
|
|
@@ -1828,9 +2747,9 @@ jobs:
|
|
|
1828
2747
|
- run: npx archtracker check --target ${opts.target} --ci
|
|
1829
2748
|
`;
|
|
1830
2749
|
try {
|
|
1831
|
-
const dir =
|
|
2750
|
+
const dir = join5(".github", "workflows");
|
|
1832
2751
|
await mkdir2(dir, { recursive: true });
|
|
1833
|
-
const path =
|
|
2752
|
+
const path = join5(dir, "arch-check.yml");
|
|
1834
2753
|
await writeFile2(path, workflow, "utf-8");
|
|
1835
2754
|
console.log(t("ci.generated", { path }));
|
|
1836
2755
|
} catch (error) {
|