@jatingargiitk/archmap 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +214 -2
- package/action.yml +21 -0
- package/dist/action.d.ts +2 -0
- package/dist/action.js +338 -0
- package/dist/action.js.map +1 -0
- package/dist/chunk-BCM7DLU6.js +904 -0
- package/dist/chunk-BCM7DLU6.js.map +1 -0
- package/dist/chunk-UVZVBM2N.js +911 -0
- package/dist/chunk-UVZVBM2N.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +350 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +110 -0
- package/dist/index.js +871 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -4
- package/skills/archmap/SKILL.md +54 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/parser/tree-sitter-loader.ts
|
|
4
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
5
|
+
var parserInitialized = false;
|
|
6
|
+
async function initParser() {
|
|
7
|
+
if (parserInitialized) return;
|
|
8
|
+
await Parser.init();
|
|
9
|
+
parserInitialized = true;
|
|
10
|
+
}
|
|
11
|
+
var languageCache = /* @__PURE__ */ new Map();
|
|
12
|
+
async function loadLanguage(name) {
|
|
13
|
+
if (languageCache.has(name)) {
|
|
14
|
+
return languageCache.get(name);
|
|
15
|
+
}
|
|
16
|
+
await initParser();
|
|
17
|
+
const wasmPath = await getWasmPath(name);
|
|
18
|
+
const language = await Language.load(wasmPath);
|
|
19
|
+
languageCache.set(name, language);
|
|
20
|
+
return language;
|
|
21
|
+
}
|
|
22
|
+
async function getWasmPath(name) {
|
|
23
|
+
try {
|
|
24
|
+
const { createRequire } = await import("module");
|
|
25
|
+
const require2 = createRequire(import.meta.url);
|
|
26
|
+
const wasmPath = require2.resolve(`tree-sitter-${name}/tree-sitter-${name}.wasm`);
|
|
27
|
+
return wasmPath;
|
|
28
|
+
} catch {
|
|
29
|
+
return `node_modules/tree-sitter-${name}/tree-sitter-${name}.wasm`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function parseFile(content, language) {
|
|
33
|
+
const lang = await loadLanguage(language);
|
|
34
|
+
const parser = new Parser();
|
|
35
|
+
parser.setLanguage(lang);
|
|
36
|
+
const tree = parser.parse(content);
|
|
37
|
+
if (!tree) {
|
|
38
|
+
throw new Error(`Failed to parse file with language: ${language}`);
|
|
39
|
+
}
|
|
40
|
+
return tree;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/parser/typescript-extractor.ts
|
|
44
|
+
async function extractTypeScriptImports(filePath, content) {
|
|
45
|
+
const tree = await parseFile(content, "typescript");
|
|
46
|
+
const imports = [];
|
|
47
|
+
visitNode(tree.rootNode, (node) => {
|
|
48
|
+
if (node.type === "import_statement") {
|
|
49
|
+
const imp = extractImportStatement(node, filePath, content);
|
|
50
|
+
if (imp) imports.push(imp);
|
|
51
|
+
} else if (node.type === "call_expression") {
|
|
52
|
+
const imp = extractDynamicImport(node, filePath, content);
|
|
53
|
+
if (imp) imports.push(imp);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return { file: filePath, imports };
|
|
57
|
+
}
|
|
58
|
+
function extractImportStatement(node, file, content) {
|
|
59
|
+
const sourceNode = node.childForFieldName("source");
|
|
60
|
+
if (!sourceNode) return null;
|
|
61
|
+
const from = extractStringValue(sourceNode, content);
|
|
62
|
+
if (!from) return null;
|
|
63
|
+
return {
|
|
64
|
+
from,
|
|
65
|
+
location: getLocation(node, file)
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function extractDynamicImport(node, file, content) {
|
|
69
|
+
const func = node.childForFieldName("function");
|
|
70
|
+
if (!func || func.text !== "import") return null;
|
|
71
|
+
const args = node.childForFieldName("arguments");
|
|
72
|
+
if (!args) return null;
|
|
73
|
+
const firstArg = args.children.find((c) => c.type === "string" || c.type === "template_string");
|
|
74
|
+
if (!firstArg) return null;
|
|
75
|
+
const from = extractStringValue(firstArg, content);
|
|
76
|
+
if (!from) return null;
|
|
77
|
+
return {
|
|
78
|
+
from,
|
|
79
|
+
isDynamic: true,
|
|
80
|
+
location: getLocation(node, file)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function extractStringValue(node, _content) {
|
|
84
|
+
const text = node.text;
|
|
85
|
+
if (text.startsWith('"') || text.startsWith("'")) {
|
|
86
|
+
return text.slice(1, -1);
|
|
87
|
+
}
|
|
88
|
+
if (text.startsWith("`")) {
|
|
89
|
+
const withoutBackticks = text.slice(1, -1);
|
|
90
|
+
if (!withoutBackticks.includes("${")) {
|
|
91
|
+
return withoutBackticks;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function getLocation(node, file) {
|
|
97
|
+
return {
|
|
98
|
+
file,
|
|
99
|
+
line: node.startPosition.row + 1,
|
|
100
|
+
column: node.startPosition.column
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function visitNode(node, visitor) {
|
|
104
|
+
visitor(node);
|
|
105
|
+
for (const child of node.children) {
|
|
106
|
+
visitNode(child, visitor);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/parser/python-extractor.ts
|
|
111
|
+
async function extractPythonImports(filePath, content) {
|
|
112
|
+
const tree = await parseFile(content, "python");
|
|
113
|
+
const imports = [];
|
|
114
|
+
visitNode2(tree.rootNode, (node) => {
|
|
115
|
+
if (node.type === "import_statement" || node.type === "import_from_statement") {
|
|
116
|
+
const imp = extractImport(node, filePath, content);
|
|
117
|
+
if (imp) imports.push(imp);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return { file: filePath, imports };
|
|
121
|
+
}
|
|
122
|
+
function extractImport(node, file, _content) {
|
|
123
|
+
if (node.type === "import_statement") {
|
|
124
|
+
return extractPlainImport(node, file);
|
|
125
|
+
} else if (node.type === "import_from_statement") {
|
|
126
|
+
return extractFromImport(node, file);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function extractPlainImport(node, file) {
|
|
131
|
+
const nameNode = node.childForFieldName("name");
|
|
132
|
+
if (!nameNode) return null;
|
|
133
|
+
const moduleName = getModuleName(nameNode);
|
|
134
|
+
if (!moduleName) return null;
|
|
135
|
+
return {
|
|
136
|
+
from: moduleName,
|
|
137
|
+
location: getLocation2(node, file)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function extractFromImport(node, file) {
|
|
141
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
142
|
+
if (!moduleNode) return null;
|
|
143
|
+
const moduleName = getModuleName(moduleNode);
|
|
144
|
+
if (!moduleName) return null;
|
|
145
|
+
return {
|
|
146
|
+
from: moduleName,
|
|
147
|
+
location: getLocation2(node, file)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function getModuleName(node) {
|
|
151
|
+
if (node.type === "dotted_name") {
|
|
152
|
+
return node.text;
|
|
153
|
+
} else if (node.type === "relative_import") {
|
|
154
|
+
return node.text;
|
|
155
|
+
} else if (node.type === "aliased_import") {
|
|
156
|
+
const nameChild = node.childForFieldName("name");
|
|
157
|
+
if (nameChild) {
|
|
158
|
+
return getModuleName(nameChild);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return node.text || null;
|
|
162
|
+
}
|
|
163
|
+
function getLocation2(node, file) {
|
|
164
|
+
return {
|
|
165
|
+
file,
|
|
166
|
+
line: node.startPosition.row + 1,
|
|
167
|
+
column: node.startPosition.column
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function visitNode2(node, visitor) {
|
|
171
|
+
visitor(node);
|
|
172
|
+
for (const child of node.children) {
|
|
173
|
+
visitNode2(child, visitor);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/resolver/index.ts
|
|
178
|
+
import * as fs from "fs/promises";
|
|
179
|
+
import * as path from "path";
|
|
180
|
+
async function resolveImports(fileImports, config) {
|
|
181
|
+
const resolved = [];
|
|
182
|
+
for (const { file, imports } of fileImports) {
|
|
183
|
+
for (const imp of imports) {
|
|
184
|
+
const resolvedPath = await resolveImport(imp.from, file, config);
|
|
185
|
+
if (resolvedPath) {
|
|
186
|
+
resolved.push({
|
|
187
|
+
from: file,
|
|
188
|
+
to: resolvedPath,
|
|
189
|
+
location: imp.location
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return resolved;
|
|
195
|
+
}
|
|
196
|
+
async function resolveImport(importPath, fromFile, config) {
|
|
197
|
+
if (isExternalModule(importPath)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const fileExt = path.extname(fromFile);
|
|
201
|
+
const isPython = fileExt === ".py";
|
|
202
|
+
if (isPython) {
|
|
203
|
+
return resolvePythonImport(importPath, fromFile, config);
|
|
204
|
+
}
|
|
205
|
+
return resolveTypeScriptImport(importPath, fromFile, config);
|
|
206
|
+
}
|
|
207
|
+
function isExternalModule(importPath) {
|
|
208
|
+
return !importPath.startsWith(".") && !importPath.startsWith("/");
|
|
209
|
+
}
|
|
210
|
+
async function resolveTypeScriptImport(importPath, fromFile, config) {
|
|
211
|
+
if (importPath.startsWith(".")) {
|
|
212
|
+
return resolveRelativePath(importPath, fromFile, config);
|
|
213
|
+
}
|
|
214
|
+
if (config.tsconfig?.paths) {
|
|
215
|
+
const aliasResolved = await resolvePathAlias(importPath, config);
|
|
216
|
+
if (aliasResolved) return aliasResolved;
|
|
217
|
+
}
|
|
218
|
+
if (config.workspaces) {
|
|
219
|
+
const workspaceResolved = await resolveWorkspace(importPath, config);
|
|
220
|
+
if (workspaceResolved) return workspaceResolved;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
async function resolveRelativePath(importPath, fromFile, config) {
|
|
225
|
+
const dir = path.dirname(fromFile);
|
|
226
|
+
let candidatePath = path.join(dir, importPath);
|
|
227
|
+
const esmExt = path.extname(candidatePath);
|
|
228
|
+
if (esmExt === ".js" || esmExt === ".mjs" || esmExt === ".cjs") {
|
|
229
|
+
const basePath = candidatePath.slice(0, -esmExt.length);
|
|
230
|
+
const tsExtensions = esmExt === ".mjs" ? [".mts"] : esmExt === ".cjs" ? [".cts"] : [".ts", ".tsx"];
|
|
231
|
+
for (const tsExt of tsExtensions) {
|
|
232
|
+
const tsPath = basePath + tsExt;
|
|
233
|
+
if (await fileExists(path.join(config.rootDir, tsPath))) {
|
|
234
|
+
return tsPath;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
candidatePath = basePath;
|
|
238
|
+
}
|
|
239
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
|
|
240
|
+
for (const ext of extensions) {
|
|
241
|
+
const withExt = candidatePath + ext;
|
|
242
|
+
if (await fileExists(path.join(config.rootDir, withExt))) {
|
|
243
|
+
return withExt;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (const ext of ["index.ts", "index.tsx", "index.js", "index.jsx"]) {
|
|
247
|
+
const indexPath = path.join(candidatePath, ext);
|
|
248
|
+
if (await fileExists(path.join(config.rootDir, indexPath))) {
|
|
249
|
+
return indexPath;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
async function resolvePathAlias(importPath, config) {
|
|
255
|
+
if (!config.tsconfig?.paths) return null;
|
|
256
|
+
const { paths, baseUrl = "." } = config.tsconfig;
|
|
257
|
+
for (const [pattern, mappings] of Object.entries(paths)) {
|
|
258
|
+
const regex = patternToRegex(pattern);
|
|
259
|
+
const match = importPath.match(regex);
|
|
260
|
+
if (match) {
|
|
261
|
+
for (const mapping of mappings) {
|
|
262
|
+
const resolved = mapping.replace("*", match[1] || "");
|
|
263
|
+
const fullPath = path.join(baseUrl, resolved);
|
|
264
|
+
const candidate = await resolveRelativePath(`./${fullPath}`, "", {
|
|
265
|
+
...config,
|
|
266
|
+
rootDir: config.rootDir
|
|
267
|
+
});
|
|
268
|
+
if (candidate) return candidate;
|
|
269
|
+
if (await fileExists(path.join(config.rootDir, fullPath))) {
|
|
270
|
+
return fullPath;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
async function resolveWorkspace(importPath, config) {
|
|
278
|
+
if (!config.workspaces) return null;
|
|
279
|
+
for (const workspace of config.workspaces) {
|
|
280
|
+
const packageJsonPath = path.join(config.rootDir, workspace, "package.json");
|
|
281
|
+
try {
|
|
282
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
283
|
+
const pkg = JSON.parse(content);
|
|
284
|
+
if (pkg.name === importPath || importPath.startsWith(pkg.name + "/")) {
|
|
285
|
+
const subpath = importPath.slice(pkg.name.length + 1) || ".";
|
|
286
|
+
const mainFile = pkg.main || pkg.module || "index.js";
|
|
287
|
+
if (subpath === ".") {
|
|
288
|
+
return path.join(workspace, mainFile);
|
|
289
|
+
}
|
|
290
|
+
const resolved = await resolveRelativePath(`./${subpath}`, workspace, {
|
|
291
|
+
...config,
|
|
292
|
+
rootDir: config.rootDir
|
|
293
|
+
});
|
|
294
|
+
if (resolved) return resolved;
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
async function resolvePythonImport(importPath, fromFile, config) {
|
|
302
|
+
if (importPath.startsWith(".")) {
|
|
303
|
+
return resolvePythonRelative(importPath, fromFile, config);
|
|
304
|
+
}
|
|
305
|
+
const parts = importPath.split(".");
|
|
306
|
+
const possiblePaths = [
|
|
307
|
+
path.join(...parts) + ".py",
|
|
308
|
+
path.join(...parts, "__init__.py"),
|
|
309
|
+
path.join("src", ...parts) + ".py",
|
|
310
|
+
path.join("src", ...parts, "__init__.py")
|
|
311
|
+
];
|
|
312
|
+
for (const possiblePath of possiblePaths) {
|
|
313
|
+
if (await fileExists(path.join(config.rootDir, possiblePath))) {
|
|
314
|
+
return possiblePath;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
async function resolvePythonRelative(importPath, fromFile, config) {
|
|
320
|
+
const dir = path.dirname(fromFile);
|
|
321
|
+
let targetDir = dir;
|
|
322
|
+
const dots = importPath.match(/^\.+/)?.[0] || ".";
|
|
323
|
+
const rest = importPath.slice(dots.length);
|
|
324
|
+
for (let i = 1; i < dots.length; i++) {
|
|
325
|
+
targetDir = path.dirname(targetDir);
|
|
326
|
+
}
|
|
327
|
+
if (rest) {
|
|
328
|
+
const parts = rest.split(".");
|
|
329
|
+
targetDir = path.join(targetDir, ...parts);
|
|
330
|
+
}
|
|
331
|
+
const candidatePath = targetDir + ".py";
|
|
332
|
+
if (await fileExists(path.join(config.rootDir, candidatePath))) {
|
|
333
|
+
return candidatePath;
|
|
334
|
+
}
|
|
335
|
+
const initPath = path.join(targetDir, "__init__.py");
|
|
336
|
+
if (await fileExists(path.join(config.rootDir, initPath))) {
|
|
337
|
+
return initPath;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function patternToRegex(pattern) {
|
|
342
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
343
|
+
const withWildcard = escaped.replace(/\\\*/g, "(.*)");
|
|
344
|
+
return new RegExp(`^${withWildcard}$`);
|
|
345
|
+
}
|
|
346
|
+
async function fileExists(filePath) {
|
|
347
|
+
try {
|
|
348
|
+
await fs.access(filePath);
|
|
349
|
+
return true;
|
|
350
|
+
} catch {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function loadTsConfig(rootDir) {
|
|
355
|
+
const tsconfigPath = path.join(rootDir, "tsconfig.json");
|
|
356
|
+
try {
|
|
357
|
+
const content = await fs.readFile(tsconfigPath, "utf-8");
|
|
358
|
+
const tsconfig = JSON.parse(content);
|
|
359
|
+
return {
|
|
360
|
+
baseUrl: tsconfig.compilerOptions?.baseUrl,
|
|
361
|
+
paths: tsconfig.compilerOptions?.paths
|
|
362
|
+
};
|
|
363
|
+
} catch {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async function findWorkspaces(rootDir) {
|
|
368
|
+
const packageJsonPath = path.join(rootDir, "package.json");
|
|
369
|
+
try {
|
|
370
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
371
|
+
const pkg = JSON.parse(content);
|
|
372
|
+
if (pkg.workspaces) {
|
|
373
|
+
const patterns = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
|
|
374
|
+
const workspaces = [];
|
|
375
|
+
for (const pattern of patterns) {
|
|
376
|
+
if (!pattern.includes("*")) {
|
|
377
|
+
workspaces.push(pattern);
|
|
378
|
+
} else {
|
|
379
|
+
const base = pattern.replace("/*", "");
|
|
380
|
+
try {
|
|
381
|
+
const entries = await fs.readdir(path.join(rootDir, base));
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
const fullPath = path.join(base, entry);
|
|
384
|
+
const pkgPath = path.join(rootDir, fullPath, "package.json");
|
|
385
|
+
if (await fileExists(pkgPath)) {
|
|
386
|
+
workspaces.push(fullPath);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return workspaces;
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/graph/index.ts
|
|
401
|
+
import * as path2 from "path";
|
|
402
|
+
function buildGraph(resolvedImports, allFiles, config = {}) {
|
|
403
|
+
const { roots = ["src", "packages/*", "apps/*"], depth = 1 } = config;
|
|
404
|
+
const fileToModule = /* @__PURE__ */ new Map();
|
|
405
|
+
for (const file of allFiles) {
|
|
406
|
+
const moduleName = getModuleName2(file, roots, depth);
|
|
407
|
+
fileToModule.set(file, moduleName);
|
|
408
|
+
}
|
|
409
|
+
const moduleFiles = /* @__PURE__ */ new Map();
|
|
410
|
+
for (const [file, module] of fileToModule) {
|
|
411
|
+
if (!moduleFiles.has(module)) {
|
|
412
|
+
moduleFiles.set(module, /* @__PURE__ */ new Set());
|
|
413
|
+
}
|
|
414
|
+
moduleFiles.get(module).add(file);
|
|
415
|
+
}
|
|
416
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
417
|
+
const externalDeps = /* @__PURE__ */ new Map();
|
|
418
|
+
for (const resolved of resolvedImports) {
|
|
419
|
+
const fromModule = fileToModule.get(resolved.from);
|
|
420
|
+
const toModule = fileToModule.get(resolved.to);
|
|
421
|
+
if (!fromModule || !toModule || fromModule === toModule) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (!edgeMap.has(fromModule)) {
|
|
425
|
+
edgeMap.set(fromModule, /* @__PURE__ */ new Map());
|
|
426
|
+
}
|
|
427
|
+
const fromEdges = edgeMap.get(fromModule);
|
|
428
|
+
if (!fromEdges.has(toModule)) {
|
|
429
|
+
fromEdges.set(toModule, {
|
|
430
|
+
from: fromModule,
|
|
431
|
+
to: toModule,
|
|
432
|
+
weight: 0,
|
|
433
|
+
locations: []
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const edge = fromEdges.get(toModule);
|
|
437
|
+
edge.weight++;
|
|
438
|
+
edge.locations.push(resolved.location);
|
|
439
|
+
}
|
|
440
|
+
const edges = [];
|
|
441
|
+
for (const fromEdges of edgeMap.values()) {
|
|
442
|
+
for (const edge of fromEdges.values()) {
|
|
443
|
+
edges.push(edge);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const fanIn = /* @__PURE__ */ new Map();
|
|
447
|
+
const fanOut = /* @__PURE__ */ new Map();
|
|
448
|
+
for (const edge of edges) {
|
|
449
|
+
fanOut.set(edge.from, (fanOut.get(edge.from) || 0) + 1);
|
|
450
|
+
fanIn.set(edge.to, (fanIn.get(edge.to) || 0) + 1);
|
|
451
|
+
}
|
|
452
|
+
const modules = [];
|
|
453
|
+
for (const [moduleName, files] of moduleFiles) {
|
|
454
|
+
modules.push({
|
|
455
|
+
name: moduleName,
|
|
456
|
+
files: Array.from(files).sort(),
|
|
457
|
+
fanIn: fanIn.get(moduleName) || 0,
|
|
458
|
+
fanOut: fanOut.get(moduleName) || 0,
|
|
459
|
+
externalDeps: Array.from(externalDeps.get(moduleName) || []).sort()
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
463
|
+
const cycles = detectCycles(edges, modules.map((m) => m.name));
|
|
464
|
+
return {
|
|
465
|
+
modules,
|
|
466
|
+
edges,
|
|
467
|
+
cycles
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function getModuleName2(file, roots, depth) {
|
|
471
|
+
const normalized = file.replace(/\\/g, "/");
|
|
472
|
+
for (const root of roots) {
|
|
473
|
+
if (root.endsWith("/*")) {
|
|
474
|
+
const base = root.slice(0, -2);
|
|
475
|
+
if (normalized.startsWith(base + "/")) {
|
|
476
|
+
const rest = normalized.slice(base.length + 1);
|
|
477
|
+
const parts2 = rest.split("/");
|
|
478
|
+
if (parts2.length > 0) {
|
|
479
|
+
return path2.join(base, parts2[0]);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} else {
|
|
483
|
+
if (normalized.startsWith(root + "/")) {
|
|
484
|
+
const rest = normalized.slice(root.length + 1);
|
|
485
|
+
const parts2 = rest.split("/");
|
|
486
|
+
if (depth === 0) {
|
|
487
|
+
return root;
|
|
488
|
+
}
|
|
489
|
+
const moduleParts = parts2.slice(0, depth);
|
|
490
|
+
return path2.join(root, ...moduleParts);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const parts = normalized.split("/");
|
|
495
|
+
if (parts.length > 0) {
|
|
496
|
+
return parts[0];
|
|
497
|
+
}
|
|
498
|
+
return normalized;
|
|
499
|
+
}
|
|
500
|
+
function detectCycles(edges, modules) {
|
|
501
|
+
const graph = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const module of modules) {
|
|
503
|
+
graph.set(module, []);
|
|
504
|
+
}
|
|
505
|
+
for (const edge of edges) {
|
|
506
|
+
if (!graph.has(edge.from)) {
|
|
507
|
+
graph.set(edge.from, []);
|
|
508
|
+
}
|
|
509
|
+
graph.get(edge.from).push(edge.to);
|
|
510
|
+
}
|
|
511
|
+
return tarjan(graph);
|
|
512
|
+
}
|
|
513
|
+
function tarjan(graph) {
|
|
514
|
+
let index = 0;
|
|
515
|
+
const stack = [];
|
|
516
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
517
|
+
const indices = /* @__PURE__ */ new Map();
|
|
518
|
+
const lowlinks = /* @__PURE__ */ new Map();
|
|
519
|
+
const sccs = [];
|
|
520
|
+
function strongConnect(v) {
|
|
521
|
+
indices.set(v, index);
|
|
522
|
+
lowlinks.set(v, index);
|
|
523
|
+
index++;
|
|
524
|
+
stack.push(v);
|
|
525
|
+
onStack.add(v);
|
|
526
|
+
const neighbors = graph.get(v) || [];
|
|
527
|
+
for (const w of neighbors) {
|
|
528
|
+
if (!indices.has(w)) {
|
|
529
|
+
strongConnect(w);
|
|
530
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
531
|
+
} else if (onStack.has(w)) {
|
|
532
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (lowlinks.get(v) === indices.get(v)) {
|
|
536
|
+
const scc = [];
|
|
537
|
+
let w;
|
|
538
|
+
do {
|
|
539
|
+
w = stack.pop();
|
|
540
|
+
onStack.delete(w);
|
|
541
|
+
scc.push(w);
|
|
542
|
+
} while (w !== v);
|
|
543
|
+
if (scc.length > 1) {
|
|
544
|
+
sccs.push(scc.sort());
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
for (const v of graph.keys()) {
|
|
549
|
+
if (!indices.has(v)) {
|
|
550
|
+
strongConnect(v);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return sccs;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/render/index.ts
|
|
557
|
+
import * as fs2 from "fs/promises";
|
|
558
|
+
import * as path3 from "path";
|
|
559
|
+
var START_MARKER = "<!-- archmap:start -->";
|
|
560
|
+
var END_MARKER = "<!-- archmap:end -->";
|
|
561
|
+
function renderMermaid(graph) {
|
|
562
|
+
const sections = [];
|
|
563
|
+
sections.push("# Architecture\n");
|
|
564
|
+
sections.push(START_MARKER);
|
|
565
|
+
sections.push("");
|
|
566
|
+
sections.push("## Module Dependencies\n");
|
|
567
|
+
sections.push("```mermaid");
|
|
568
|
+
sections.push(renderMermaidGraph(graph));
|
|
569
|
+
sections.push("```\n");
|
|
570
|
+
sections.push("## Modules\n");
|
|
571
|
+
sections.push(renderModuleTable(graph.modules));
|
|
572
|
+
sections.push("");
|
|
573
|
+
if (graph.cycles.length > 0) {
|
|
574
|
+
sections.push("## \u26A0\uFE0F Cycles Detected\n");
|
|
575
|
+
sections.push(renderCycles(graph.cycles));
|
|
576
|
+
sections.push("");
|
|
577
|
+
}
|
|
578
|
+
sections.push(END_MARKER);
|
|
579
|
+
return sections.join("\n");
|
|
580
|
+
}
|
|
581
|
+
function renderMermaidGraph(graph) {
|
|
582
|
+
const lines = ["flowchart LR"];
|
|
583
|
+
const edgesSorted = [...graph.edges].sort((a, b) => {
|
|
584
|
+
if (a.from !== b.from) return a.from.localeCompare(b.from);
|
|
585
|
+
return a.to.localeCompare(b.to);
|
|
586
|
+
});
|
|
587
|
+
for (const edge of edgesSorted) {
|
|
588
|
+
const fromId = sanitizeId(edge.from);
|
|
589
|
+
const toId = sanitizeId(edge.to);
|
|
590
|
+
const label = edge.weight > 1 ? ` |${edge.weight}| ` : " ";
|
|
591
|
+
lines.push(` ${fromId}[${edge.from}]${label}-->${label}${toId}[${edge.to}]`);
|
|
592
|
+
}
|
|
593
|
+
if (edgesSorted.length === 0) {
|
|
594
|
+
for (const module of graph.modules.slice(0, 5)) {
|
|
595
|
+
const id = sanitizeId(module.name);
|
|
596
|
+
lines.push(` ${id}[${module.name}]`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return lines.join("\n");
|
|
600
|
+
}
|
|
601
|
+
function renderModuleTable(modules) {
|
|
602
|
+
const lines = [
|
|
603
|
+
"| Module | Files | Fan-In | Fan-Out | External Deps |",
|
|
604
|
+
"|--------|-------|--------|---------|---------------|"
|
|
605
|
+
];
|
|
606
|
+
const sorted = [...modules].sort((a, b) => {
|
|
607
|
+
const fanA = a.fanIn + a.fanOut;
|
|
608
|
+
const fanB = b.fanIn + b.fanOut;
|
|
609
|
+
if (fanB !== fanA) return fanB - fanA;
|
|
610
|
+
return a.name.localeCompare(b.name);
|
|
611
|
+
});
|
|
612
|
+
for (const module of sorted) {
|
|
613
|
+
const externalDeps = module.externalDeps.length > 0 ? module.externalDeps.slice(0, 3).join(", ") + (module.externalDeps.length > 3 ? "..." : "") : "-";
|
|
614
|
+
lines.push(
|
|
615
|
+
`| \`${module.name}\` | ${module.files.length} | ${module.fanIn} | ${module.fanOut} | ${externalDeps} |`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return lines.join("\n");
|
|
619
|
+
}
|
|
620
|
+
function renderCycles(cycles) {
|
|
621
|
+
const lines = [];
|
|
622
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
623
|
+
const cycle = cycles[i];
|
|
624
|
+
lines.push(`${i + 1}. ${cycle.map((m) => `\`${m}\``).join(" \u2192 ")}`);
|
|
625
|
+
}
|
|
626
|
+
return lines.join("\n");
|
|
627
|
+
}
|
|
628
|
+
function sanitizeId(name) {
|
|
629
|
+
return name.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
630
|
+
}
|
|
631
|
+
async function writeArchitectureFile(filePath, graph) {
|
|
632
|
+
let existingContent = "";
|
|
633
|
+
try {
|
|
634
|
+
existingContent = await fs2.readFile(filePath, "utf-8");
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
const newContent = renderMermaid(graph);
|
|
638
|
+
let finalContent;
|
|
639
|
+
if (existingContent) {
|
|
640
|
+
const startIndex = existingContent.indexOf(START_MARKER);
|
|
641
|
+
const endIndex = existingContent.indexOf(END_MARKER);
|
|
642
|
+
if (startIndex !== -1 && endIndex !== -1) {
|
|
643
|
+
const before = existingContent.slice(0, startIndex);
|
|
644
|
+
const after = existingContent.slice(endIndex + END_MARKER.length);
|
|
645
|
+
const extractedSection = newContent.slice(
|
|
646
|
+
newContent.indexOf(START_MARKER),
|
|
647
|
+
newContent.indexOf(END_MARKER) + END_MARKER.length
|
|
648
|
+
);
|
|
649
|
+
finalContent = before + extractedSection + after;
|
|
650
|
+
} else {
|
|
651
|
+
finalContent = newContent + "\n\n" + existingContent;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
finalContent = newContent;
|
|
655
|
+
}
|
|
656
|
+
await fs2.mkdir(path3.dirname(filePath), { recursive: true });
|
|
657
|
+
await fs2.writeFile(filePath, finalContent, "utf-8");
|
|
658
|
+
}
|
|
659
|
+
function renderDeltaDiagram(addedEdges, removedEdges, touchedModules) {
|
|
660
|
+
const lines = ["```mermaid", "flowchart LR"];
|
|
661
|
+
for (const module of touchedModules) {
|
|
662
|
+
const id = sanitizeId(module);
|
|
663
|
+
lines.push(` ${id}[${module}]`);
|
|
664
|
+
}
|
|
665
|
+
for (const edge of addedEdges) {
|
|
666
|
+
const fromId = sanitizeId(edge.from);
|
|
667
|
+
const toId = sanitizeId(edge.to);
|
|
668
|
+
lines.push(` ${fromId} ====> ${toId}`);
|
|
669
|
+
lines.push(` linkStyle ${lines.length - 2} stroke:#00ff00,stroke-width:3px`);
|
|
670
|
+
}
|
|
671
|
+
for (const edge of removedEdges) {
|
|
672
|
+
const fromId = sanitizeId(edge.from);
|
|
673
|
+
const toId = sanitizeId(edge.to);
|
|
674
|
+
lines.push(` ${fromId} -.-> ${toId}`);
|
|
675
|
+
lines.push(` linkStyle ${lines.length - 2} stroke:#ff0000,stroke-width:2px,stroke-dasharray:5`);
|
|
676
|
+
}
|
|
677
|
+
lines.push("```");
|
|
678
|
+
return lines.join("\n");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/lock/index.ts
|
|
682
|
+
import * as fs3 from "fs/promises";
|
|
683
|
+
var LOCK_VERSION = "1";
|
|
684
|
+
function graphToLock(graph) {
|
|
685
|
+
const modules = graph.modules.map((m) => m.name).sort();
|
|
686
|
+
const edges = graph.edges.map((e) => ({
|
|
687
|
+
from: e.from,
|
|
688
|
+
to: e.to,
|
|
689
|
+
weight: e.weight
|
|
690
|
+
})).sort((a, b) => {
|
|
691
|
+
if (a.from !== b.from) return a.from.localeCompare(b.from);
|
|
692
|
+
return a.to.localeCompare(b.to);
|
|
693
|
+
});
|
|
694
|
+
const externalDeps = {};
|
|
695
|
+
for (const module of graph.modules) {
|
|
696
|
+
if (module.externalDeps.length > 0) {
|
|
697
|
+
externalDeps[module.name] = module.externalDeps.sort();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
version: LOCK_VERSION,
|
|
702
|
+
modules,
|
|
703
|
+
edges,
|
|
704
|
+
externalDeps
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
async function writeLockFile(filePath, lock) {
|
|
708
|
+
const content = JSON.stringify(lock, null, 2) + "\n";
|
|
709
|
+
await fs3.writeFile(filePath, content, "utf-8");
|
|
710
|
+
}
|
|
711
|
+
async function readLockFile(filePath) {
|
|
712
|
+
try {
|
|
713
|
+
const content = await fs3.readFile(filePath, "utf-8");
|
|
714
|
+
return JSON.parse(content);
|
|
715
|
+
} catch {
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function diffLocks(oldLock, newLock) {
|
|
720
|
+
const oldEdges = oldLock?.edges || [];
|
|
721
|
+
const newEdges = newLock.edges;
|
|
722
|
+
const oldEdgeSet = new Set(oldEdges.map(edgeKey));
|
|
723
|
+
const newEdgeSet = new Set(newEdges.map(edgeKey));
|
|
724
|
+
const added = [];
|
|
725
|
+
const removed = [];
|
|
726
|
+
for (const edge of newEdges) {
|
|
727
|
+
if (!oldEdgeSet.has(edgeKey(edge))) {
|
|
728
|
+
added.push({
|
|
729
|
+
from: edge.from,
|
|
730
|
+
to: edge.to,
|
|
731
|
+
weight: edge.weight,
|
|
732
|
+
locations: []
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
for (const edge of oldEdges) {
|
|
737
|
+
if (!newEdgeSet.has(edgeKey(edge))) {
|
|
738
|
+
removed.push({
|
|
739
|
+
from: edge.from,
|
|
740
|
+
to: edge.to,
|
|
741
|
+
weight: edge.weight,
|
|
742
|
+
locations: []
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
const newCycles = detectNewCycles(oldLock, newLock);
|
|
747
|
+
return {
|
|
748
|
+
added,
|
|
749
|
+
removed,
|
|
750
|
+
newCycles
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function edgeKey(edge) {
|
|
754
|
+
return `${edge.from}->${edge.to}`;
|
|
755
|
+
}
|
|
756
|
+
function detectNewCycles(oldLock, newLock) {
|
|
757
|
+
if (!oldLock) {
|
|
758
|
+
return [];
|
|
759
|
+
}
|
|
760
|
+
const oldCycles = findCyclesInLock(oldLock);
|
|
761
|
+
const newCycles = findCyclesInLock(newLock);
|
|
762
|
+
const oldCycleSet = new Set(oldCycles.map(cycleKey));
|
|
763
|
+
const result = [];
|
|
764
|
+
for (const cycle of newCycles) {
|
|
765
|
+
if (!oldCycleSet.has(cycleKey(cycle))) {
|
|
766
|
+
result.push(cycle);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return result;
|
|
770
|
+
}
|
|
771
|
+
function findCyclesInLock(lock) {
|
|
772
|
+
const graph = /* @__PURE__ */ new Map();
|
|
773
|
+
for (const module of lock.modules) {
|
|
774
|
+
graph.set(module, []);
|
|
775
|
+
}
|
|
776
|
+
for (const edge of lock.edges) {
|
|
777
|
+
if (!graph.has(edge.from)) {
|
|
778
|
+
graph.set(edge.from, []);
|
|
779
|
+
}
|
|
780
|
+
graph.get(edge.from).push(edge.to);
|
|
781
|
+
}
|
|
782
|
+
return tarjan2(graph);
|
|
783
|
+
}
|
|
784
|
+
function tarjan2(graph) {
|
|
785
|
+
let index = 0;
|
|
786
|
+
const stack = [];
|
|
787
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
788
|
+
const indices = /* @__PURE__ */ new Map();
|
|
789
|
+
const lowlinks = /* @__PURE__ */ new Map();
|
|
790
|
+
const sccs = [];
|
|
791
|
+
function strongConnect(v) {
|
|
792
|
+
indices.set(v, index);
|
|
793
|
+
lowlinks.set(v, index);
|
|
794
|
+
index++;
|
|
795
|
+
stack.push(v);
|
|
796
|
+
onStack.add(v);
|
|
797
|
+
const neighbors = graph.get(v) || [];
|
|
798
|
+
for (const w of neighbors) {
|
|
799
|
+
if (!indices.has(w)) {
|
|
800
|
+
strongConnect(w);
|
|
801
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
802
|
+
} else if (onStack.has(w)) {
|
|
803
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (lowlinks.get(v) === indices.get(v)) {
|
|
807
|
+
const scc = [];
|
|
808
|
+
let w;
|
|
809
|
+
do {
|
|
810
|
+
w = stack.pop();
|
|
811
|
+
onStack.delete(w);
|
|
812
|
+
scc.push(w);
|
|
813
|
+
} while (w !== v);
|
|
814
|
+
if (scc.length > 1) {
|
|
815
|
+
sccs.push(scc.sort());
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
for (const v of graph.keys()) {
|
|
820
|
+
if (!indices.has(v)) {
|
|
821
|
+
strongConnect(v);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return sccs;
|
|
825
|
+
}
|
|
826
|
+
function cycleKey(cycle) {
|
|
827
|
+
return cycle.sort().join("->");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// src/rules/index.ts
|
|
831
|
+
function checkRules(edges, config) {
|
|
832
|
+
const violations = [];
|
|
833
|
+
if (!config.rules) {
|
|
834
|
+
return violations;
|
|
835
|
+
}
|
|
836
|
+
for (const rule of config.rules) {
|
|
837
|
+
if (rule.deny) {
|
|
838
|
+
const ruleViolations = checkDenyRule(edges, rule.deny);
|
|
839
|
+
violations.push(...ruleViolations);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return violations;
|
|
843
|
+
}
|
|
844
|
+
function checkDenyRule(edges, deny) {
|
|
845
|
+
const violations = [];
|
|
846
|
+
for (const edge of edges) {
|
|
847
|
+
if (matchesPattern(edge.from, deny.from) && matchesPattern(edge.to, deny.to)) {
|
|
848
|
+
violations.push({
|
|
849
|
+
rule: `deny: ${deny.from} \u2192 ${deny.to}`,
|
|
850
|
+
edge,
|
|
851
|
+
message: `Module ${edge.from} is not allowed to depend on ${edge.to}`
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return violations;
|
|
856
|
+
}
|
|
857
|
+
function matchesPattern(modulePath, pattern) {
|
|
858
|
+
if (pattern.endsWith("/**")) {
|
|
859
|
+
const basePattern = pattern.slice(0, -3);
|
|
860
|
+
const regex2 = patternToRegex2(basePattern);
|
|
861
|
+
if (regex2.test(modulePath) || regex2.test(modulePath + "/")) {
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
const parts = modulePath.split("/");
|
|
865
|
+
for (let i = 0; i < parts.length; i++) {
|
|
866
|
+
const segment = parts.slice(i).join("/");
|
|
867
|
+
if (regex2.test(segment) || regex2.test(segment + "/")) {
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const regex = patternToRegex2(pattern);
|
|
873
|
+
return regex.test(modulePath) || regex.test(modulePath + "/");
|
|
874
|
+
}
|
|
875
|
+
function patternToRegex2(pattern) {
|
|
876
|
+
const regexStr = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLESTAR___").replace(/\*/g, "[^/]*").replace(/___DOUBLESTAR___/g, ".*");
|
|
877
|
+
return new RegExp(`^${regexStr}$`);
|
|
878
|
+
}
|
|
879
|
+
function shouldFailCheck(diff, violations, config) {
|
|
880
|
+
if (config.mode === "gate") {
|
|
881
|
+
if (diff.added.length > 0 || diff.removed.length > 0) {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return violations.length > 0;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
export {
|
|
889
|
+
extractTypeScriptImports,
|
|
890
|
+
extractPythonImports,
|
|
891
|
+
resolveImports,
|
|
892
|
+
loadTsConfig,
|
|
893
|
+
findWorkspaces,
|
|
894
|
+
buildGraph,
|
|
895
|
+
writeArchitectureFile,
|
|
896
|
+
renderDeltaDiagram,
|
|
897
|
+
graphToLock,
|
|
898
|
+
writeLockFile,
|
|
899
|
+
readLockFile,
|
|
900
|
+
diffLocks,
|
|
901
|
+
checkRules,
|
|
902
|
+
shouldFailCheck
|
|
903
|
+
};
|
|
904
|
+
//# sourceMappingURL=chunk-BCM7DLU6.js.map
|