arcvision 0.2.0 → 0.2.2

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.
@@ -35,6 +35,7 @@ async function scan(directory) {
35
35
  };
36
36
 
37
37
  const fileMap = new Map();
38
+ let totalImportsFound = 0; // Track total imports found
38
39
 
39
40
  // Process files with plugins
40
41
  for (const file of files) {
@@ -70,6 +71,11 @@ async function scan(directory) {
70
71
 
71
72
  // Process with plugins
72
73
  metadata = await pluginManager.processFile(file, metadata);
74
+
75
+ // Count imports found in this file
76
+ if (metadata.imports && Array.isArray(metadata.imports)) {
77
+ totalImportsFound += metadata.imports.length;
78
+ }
73
79
 
74
80
  const node = {
75
81
  id: normalizedRelativePath,
@@ -88,35 +94,293 @@ async function scan(directory) {
88
94
  // Load tsconfig for path resolution
89
95
  const tsconfig = loadTSConfig(directory);
90
96
 
91
- const normalizedFileMap = new Map();
92
- architectureMap.nodes.forEach(n => normalizedFileMap.set(normalize(n.id), n.id));
93
-
97
+ // Create a mapping of all possible normalized paths that could match imports
98
+ // This includes both the direct file paths and possible variations
99
+ const allPossiblePaths = new Set();
100
+ const pathToNodeIdMap = new Map(); // This maps normalized paths to node IDs
101
+
94
102
  architectureMap.nodes.forEach(node => {
95
- node.metadata.imports.forEach(imp => {
96
- // Resolve the import path using the new resolver
97
- const resolvedPath = resolveImport(
98
- imp.source,
99
- path.join(directory, node.id),
100
- directory,
101
- tsconfig
102
- );
103
-
104
- if (resolvedPath) {
105
- // Convert resolved absolute path back to relative path
106
- const relativeResolvedPath = path.relative(directory, resolvedPath);
107
- const normalizedResolvedPath = normalize(relativeResolvedPath);
108
-
109
- // Check if the resolved file exists in our scanned files
110
- if (normalizedFileMap.has(normalizedResolvedPath)) {
111
- architectureMap.edges.push({
112
- source: normalize(node.id),
113
- target: normalizedResolvedPath,
114
- type: 'import'
115
- });
103
+ const normalizedPath = normalize(node.id);
104
+ allPossiblePaths.add(normalizedPath);
105
+ pathToNodeIdMap.set(normalizedPath, node.id);
106
+ });
107
+
108
+ // Process imports to create edges
109
+ let unresolvedImports = 0;
110
+ let resolvedImports = 0;
111
+ architectureMap.nodes.forEach(node => {
112
+ if (node.metadata.imports && Array.isArray(node.metadata.imports)) {
113
+ node.metadata.imports.forEach(imp => {
114
+ if (imp.source && typeof imp.source === 'string') {
115
+ let targetFound = false;
116
+
117
+ try {
118
+ // First, check if imp.source directly matches any node path (exact match)
119
+ if (allPossiblePaths.has(imp.source)) {
120
+ architectureMap.edges.push({
121
+ source: normalize(node.id),
122
+ target: imp.source,
123
+ type: imp.type || 'import'
124
+ });
125
+ resolvedImports++;
126
+ targetFound = true;
127
+ }
128
+
129
+ if (!targetFound) {
130
+ // Try to resolve the import path using the resolver
131
+ const resolvedPath = resolveImport(
132
+ imp.source,
133
+ path.join(directory, node.id),
134
+ directory,
135
+ tsconfig
136
+ );
137
+
138
+ if (resolvedPath) {
139
+ // Convert resolved absolute path back to relative path
140
+ const relativeResolvedPath = path.relative(directory, resolvedPath);
141
+ const normalizedResolvedPath = normalize(relativeResolvedPath);
142
+
143
+ // Check if the resolved file exists in our scanned files
144
+ if (allPossiblePaths.has(normalizedResolvedPath)) {
145
+ architectureMap.edges.push({
146
+ source: normalize(node.id),
147
+ target: normalizedResolvedPath,
148
+ type: imp.type || 'import'
149
+ });
150
+ resolvedImports++;
151
+ targetFound = true;
152
+ }
153
+ }
154
+ }
155
+
156
+ if (!targetFound) {
157
+ // As a fallback, check if the import path could match by simple relative path calculation
158
+ // For example, if node is 'src/core/scanner.js' and import is './parser.js',
159
+ // it should resolve to 'src/core/parser.js'
160
+ try {
161
+ const baseDir = path.dirname(path.join(directory, node.id));
162
+ const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
163
+ const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
164
+ const calculatedNormalizedPath = normalize(calculatedRelativePath);
165
+
166
+ if (allPossiblePaths.has(calculatedNormalizedPath)) {
167
+ architectureMap.edges.push({
168
+ source: normalize(node.id),
169
+ target: calculatedNormalizedPath,
170
+ type: imp.type || 'import'
171
+ });
172
+ resolvedImports++;
173
+ targetFound = true;
174
+ }
175
+ } catch (e) {
176
+ console.warn(`⚠️ Path calculation failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
177
+ // If path calculation fails, continue to unresolved
178
+ }
179
+ }
180
+
181
+ if (!targetFound) {
182
+ // Another fallback: if import starts with ./ or ../, try to find a match
183
+ // by appending common extensions to the import path
184
+ if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
185
+ // Try various extensions that might match
186
+ const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
187
+
188
+ for (const ext of extensions) {
189
+ let testPath = imp.source;
190
+ if (!imp.source.endsWith(ext)) {
191
+ testPath = imp.source + ext;
192
+ }
193
+
194
+ // Calculate the relative path from the project directory
195
+ try {
196
+ const baseDir = path.dirname(path.join(directory, node.id));
197
+ const calculatedAbsolutePath = path.resolve(baseDir, testPath);
198
+ const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
199
+ const calculatedNormalizedPath = normalize(calculatedRelativePath);
200
+
201
+ if (allPossiblePaths.has(calculatedNormalizedPath)) {
202
+ architectureMap.edges.push({
203
+ source: normalize(node.id),
204
+ target: calculatedNormalizedPath,
205
+ type: imp.type || 'import'
206
+ });
207
+ resolvedImports++;
208
+ targetFound = true;
209
+ break; // Found a match, exit the loop
210
+ }
211
+ } catch (e) {
212
+ console.warn(`⚠️ Extension path calculation failed for import '${imp.source}' with extension '${ext}' in file '${node.id}': ${e.message}`);
213
+ // Continue to next extension if path calculation fails
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ if (!targetFound) {
220
+ // Additional fallback: Check for index files when importing a directory
221
+ if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
222
+ try {
223
+ const baseDir = path.dirname(path.join(directory, node.id));
224
+ const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
225
+ const directoryPath = calculatedAbsolutePath;
226
+
227
+ // Check for index files in the directory
228
+ const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
229
+ for (const indexFile of indexFiles) {
230
+ const indexPath = path.join(directoryPath, indexFile);
231
+ const indexRelativePath = path.relative(directory, indexPath);
232
+ const indexNormalizedPath = normalize(indexRelativePath);
233
+
234
+ if (allPossiblePaths.has(indexNormalizedPath)) {
235
+ architectureMap.edges.push({
236
+ source: normalize(node.id),
237
+ target: indexNormalizedPath,
238
+ type: imp.type || 'import'
239
+ });
240
+ resolvedImports++;
241
+ targetFound = true;
242
+ break; // Found a match, exit the loop
243
+ }
244
+ }
245
+ } catch (e) {
246
+ console.warn(`⚠️ Index file check failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
247
+ // Continue if index file check fails
248
+ }
249
+ }
250
+ }
251
+
252
+ if (!targetFound) {
253
+ // Additional fallback: Check for absolute imports that might map to common directories
254
+ // For example, if tsconfig has paths like "@/*": ["src/*"], an import like "@/utils/helper"
255
+ // should resolve to "src/utils/helper.js" or similar
256
+ if (imp.source.startsWith('@') || imp.source.startsWith('/') || !imp.source.startsWith('.')) {
257
+ // Try to match against common source directories
258
+ const commonSourceDirs = ['src', 'app', 'lib', 'components', 'utils', 'services', 'assets'];
259
+
260
+ for (const srcDir of commonSourceDirs) {
261
+ // Try appending to common source directories
262
+ const potentialPath = path.join(srcDir, imp.source);
263
+ const potentialRelativePath = path.relative(directory, path.resolve(directory, potentialPath));
264
+ const potentialNormalizedPath = normalize(potentialRelativePath);
265
+
266
+ if (allPossiblePaths.has(potentialNormalizedPath)) {
267
+ architectureMap.edges.push({
268
+ source: normalize(node.id),
269
+ target: potentialNormalizedPath,
270
+ type: imp.type || 'import'
271
+ });
272
+ resolvedImports++;
273
+ targetFound = true;
274
+ break; // Found a match, exit the loop
275
+ }
276
+
277
+ // Also try with extensions
278
+ const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
279
+ 'index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
280
+
281
+ for (const ext of extensions) {
282
+ let testPath;
283
+ if (ext.startsWith('index')) {
284
+ testPath = path.join(srcDir, imp.source, ext);
285
+ } else {
286
+ testPath = path.join(srcDir, imp.source + ext);
287
+ }
288
+
289
+ const testRelativePath = path.relative(directory, path.resolve(directory, testPath));
290
+ const testNormalizedPath = normalize(testRelativePath);
291
+
292
+ if (allPossiblePaths.has(testNormalizedPath)) {
293
+ architectureMap.edges.push({
294
+ source: normalize(node.id),
295
+ target: testNormalizedPath,
296
+ type: imp.type || 'import'
297
+ });
298
+ resolvedImports++;
299
+ targetFound = true;
300
+ break; // Found a match, exit the loops
301
+ }
302
+ }
303
+
304
+ if (targetFound) break;
305
+ }
306
+ }
307
+ }
308
+
309
+ if (!targetFound) {
310
+ // Additional fallback: Try to handle barrel files (index.js that exports from other files)
311
+ // If import is '@/components/Header' but only '@/components/index.js' exists
312
+ if (imp.source.startsWith('@') || imp.source.startsWith('/')) {
313
+ try {
314
+ // Try to find a directory with an index file that matches
315
+ const parts = imp.source.split('/');
316
+ if (parts.length > 1) {
317
+ // Remove the last part and add index
318
+ const dirParts = parts.slice(0, -1);
319
+ const fileName = parts[parts.length - 1];
320
+
321
+ // Look for patterns like: components/index.js exporting { Header }
322
+ // or components/index.js containing export * from './Header'
323
+ const dirPath = dirParts.join('/') + '/index';
324
+ const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
325
+
326
+ for (const indexFile of indexFiles) {
327
+ const indexPath = path.join(dirParts.join('/'), indexFile);
328
+ const indexRelativePath = path.relative(directory, path.resolve(directory, indexPath));
329
+ const indexNormalizedPath = normalize(indexRelativePath);
330
+
331
+ if (allPossiblePaths.has(indexNormalizedPath)) {
332
+ architectureMap.edges.push({
333
+ source: normalize(node.id),
334
+ target: indexNormalizedPath,
335
+ type: imp.type || 'import'
336
+ });
337
+ resolvedImports++;
338
+ targetFound = true;
339
+ break; // Found a match, exit the loop
340
+ }
341
+ }
342
+ }
343
+ } catch (e) {
344
+ console.warn(`⚠️ Barrel file check failed for import '${imp.source}' in file '${node.id}': ${e.message}`);
345
+ // Continue if barrel file check fails
346
+ }
347
+ }
348
+ }
349
+
350
+ if (!targetFound) {
351
+ // Additional fallback: Check for exact path matches with different capitalization
352
+ // This handles cases where import uses different casing than actual file
353
+ for (const possiblePath of allPossiblePaths) {
354
+ if (possiblePath.toLowerCase() === imp.source.toLowerCase()) {
355
+ architectureMap.edges.push({
356
+ source: normalize(node.id),
357
+ target: possiblePath,
358
+ type: imp.type || 'import'
359
+ });
360
+ resolvedImports++;
361
+ targetFound = true;
362
+ break; // Found a case-insensitive match
363
+ }
364
+ }
365
+ }
366
+ } catch (e) {
367
+ console.error(`❌ Critical error processing import '${imp.source}' in file '${node.id}': ${e.message}`);
368
+ console.error(e.stack);
369
+ // Even if there's an error processing this import, increment unresolved counter
370
+ unresolvedImports++;
371
+ }
372
+
373
+ if (!targetFound) {
374
+ // Track imports that couldn't be matched to existing nodes
375
+ unresolvedImports++;
376
+ }
116
377
  }
117
- }
118
- });
378
+ });
379
+ }
119
380
  });
381
+ console.log('RESOLVED IMPORTS:', resolvedImports);
382
+ console.log('UNRESOLVED IMPORTS:', unresolvedImports);
383
+ console.log('EDGES CREATED:', architectureMap.edges.length);
120
384
 
121
385
  // Calculate blast radius for each file
122
386
  const reverseGraph = buildReverseDependencyGraph(architectureMap);
@@ -127,6 +391,39 @@ async function scan(directory) {
127
391
  node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
128
392
  });
129
393
 
394
+ console.log('BEFORE DEDUPLICATION EDGES:', architectureMap.edges.length);
395
+
396
+ // Process edges to ensure they match existing nodes and remove duplicates
397
+ const validEdges = [];
398
+ const edgeSet = new Set(); // To track unique edges
399
+
400
+ architectureMap.edges.forEach(edge => {
401
+ // Normalize both source and target paths for consistent matching
402
+ const normalizedSource = normalize(edge.source);
403
+ const normalizedTarget = normalize(edge.target);
404
+
405
+ // Create a unique key for the edge to avoid duplicates
406
+ const edgeKey = `${normalizedSource}→${normalizedTarget}`;
407
+
408
+ // Check if both source and target nodes exist in our architecture map
409
+ const sourceNodeExists = architectureMap.nodes.some(node => node.id === normalizedSource);
410
+ const targetNodeExists = architectureMap.nodes.some(node => node.id === normalizedTarget);
411
+
412
+ if (sourceNodeExists && targetNodeExists && !edgeSet.has(edgeKey)) {
413
+ edgeSet.add(edgeKey);
414
+ validEdges.push({
415
+ source: normalizedSource,
416
+ target: normalizedTarget,
417
+ type: edge.type
418
+ });
419
+ }
420
+ });
421
+
422
+ console.log('AFTER DEDUPLICATION EDGES:', validEdges.length);
423
+
424
+ // Replace the edges with only valid ones
425
+ architectureMap.edges = validEdges;
426
+
130
427
  // Add top blast radius files to the architecture map
131
428
  const totalFiles = architectureMap.nodes.length;
132
429
  const { computeBlastRadiusWithPercentage, analyzeCriticality } = require('./blastRadius');
@@ -33,12 +33,17 @@ function buildContext(files, edges, options = {}) {
33
33
  role = 'Interface';
34
34
  }
35
35
 
36
- // Extract dependencies more accurately
36
+ // Extract dependencies more accurately and remove duplicates
37
37
  const dependencies = [];
38
+ const uniqueDeps = new Set();
38
39
  if (file.metadata.imports && Array.isArray(file.metadata.imports)) {
39
40
  file.metadata.imports.forEach(imp => {
40
41
  if (imp.source && typeof imp.source === 'string') {
41
- dependencies.push(imp.source);
42
+ // Only add unique dependencies
43
+ if (!uniqueDeps.has(imp.source)) {
44
+ uniqueDeps.add(imp.source);
45
+ dependencies.push(imp.source);
46
+ }
42
47
  }
43
48
  });
44
49
  }
@@ -60,13 +65,33 @@ function buildContext(files, edges, options = {}) {
60
65
  const sourceNode = nodes.find(n => n.path === edge.source);
61
66
  const targetNode = nodes.find(n => n.path === edge.target);
62
67
 
63
- // Only include edges where both source and target nodes exist
64
- if (sourceNode && targetNode) {
65
- schemaEdges.push({
66
- from: sourceNode.id,
67
- to: targetNode.id,
68
- relation: 'imports'
69
- });
68
+ // Include edges where source exists (even if target doesn't exist as a node)
69
+ if (sourceNode) {
70
+ // Normalize the edge type to fit the schema
71
+ let relationType = 'imports';
72
+ if (edge.type === 'imports' || edge.type === 'require' || edge.type === 'export-from' || edge.type === 'export-all' || edge.type === 'dynamic-import' || edge.type === 'require-assignment') {
73
+ relationType = 'imports';
74
+ } else if (edge.type === 'calls') {
75
+ relationType = 'calls';
76
+ } else if (edge.type === 'owns') {
77
+ relationType = 'owns';
78
+ } else if (edge.type === 'depends_on') {
79
+ relationType = 'depends_on';
80
+ }
81
+
82
+ if (targetNode) {
83
+ // Both source and target nodes exist
84
+ schemaEdges.push({
85
+ from: sourceNode.id,
86
+ to: targetNode.id,
87
+ relation: relationType
88
+ });
89
+ } else {
90
+ // Source exists but target doesn't (external dependency like node_modules)
91
+ // We'll still include the edge to preserve the relationship
92
+ // However, since the schema requires valid node IDs, we'll only add if the target is a known node
93
+ // For now, we'll skip edges to non-existent targets to maintain schema compliance
94
+ }
70
95
  }
71
96
  }
72
97
 
@@ -27,7 +27,8 @@ class PluginManager {
27
27
  enhancedMetadata = { ...enhancedMetadata, ...result };
28
28
  }
29
29
  } catch (error) {
30
- console.warn(`Plugin ${plugin.name} failed: ${error.message}`);
30
+ console.warn(`Plugin ${plugin.name} failed: ${error.message || error}`);
31
+ // Continue processing with original metadata if plugin fails
31
32
  }
32
33
  }
33
34