arcvision 0.1.12 → 0.1.14
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/README.md +3 -0
- package/dist/index.js +127 -65
- package/docs/blast-radius-implementation.md +76 -0
- package/docs/blast-radius.md +44 -0
- package/package.json +1 -1
- package/src/core/blastRadius.js +84 -0
- package/src/core/path-resolver.js +84 -101
- package/src/core/scanner.js +10 -0
- package/src/index.js +29 -0
package/README.md
CHANGED
|
@@ -35,12 +35,15 @@ Scan the current directory and generate architecture map.
|
|
|
35
35
|
Options:
|
|
36
36
|
- `-u, --upload`: Upload to database
|
|
37
37
|
|
|
38
|
+
The scan command will also output a blast radius analysis showing the file with the highest blast radius and its potential impact on your codebase.
|
|
39
|
+
|
|
38
40
|
## Features
|
|
39
41
|
|
|
40
42
|
- **Modern Import Resolution**: Handles path aliases (`@/components/*`), barrel files (`./utils` → `./utils/index.ts`), and directory imports
|
|
41
43
|
- **AST-based Parsing**: Uses Babel parser for accurate import detection
|
|
42
44
|
- **Multi-framework Support**: Works with React, Next.js, and other modern JavaScript frameworks
|
|
43
45
|
- **Dependency Graph Generation**: Creates comprehensive node-edge relationships
|
|
46
|
+
- **Blast Radius Analysis**: Identifies high-risk files that are imported by many other files
|
|
44
47
|
- **Cloud Integration**: Uploads results to ArcVision dashboard for visualization
|
|
45
48
|
|
|
46
49
|
## Supported Import Patterns
|
package/dist/index.js
CHANGED
|
@@ -56442,92 +56442,127 @@ var require_path_resolver = __commonJS({
|
|
|
56442
56442
|
"src/core/path-resolver.js"(exports2, module2) {
|
|
56443
56443
|
var fs2 = require("fs");
|
|
56444
56444
|
var path2 = require("path");
|
|
56445
|
-
function resolveImport(importPath,
|
|
56446
|
-
if (importPath.startsWith("
|
|
56445
|
+
function resolveImport(importPath, importerPath, projectRoot, tsconfig) {
|
|
56446
|
+
if (importPath.startsWith("http://") || importPath.startsWith("https://") || importPath.startsWith("data:")) {
|
|
56447
56447
|
return null;
|
|
56448
56448
|
}
|
|
56449
|
-
if (
|
|
56450
|
-
|
|
56451
|
-
|
|
56452
|
-
|
|
56453
|
-
|
|
56454
|
-
|
|
56449
|
+
if (importPath.startsWith(".")) {
|
|
56450
|
+
let resolvedPath = path2.resolve(path2.dirname(importerPath), importPath);
|
|
56451
|
+
const extensions = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
56452
|
+
for (const ext of extensions) {
|
|
56453
|
+
if (fs2.existsSync(resolvedPath + ext)) {
|
|
56454
|
+
return resolvedPath + ext;
|
|
56455
|
+
}
|
|
56456
|
+
}
|
|
56457
|
+
const indexFiles = ["index.js", "index.jsx", "index.ts", "index.tsx"];
|
|
56458
|
+
for (const idxFile of indexFiles) {
|
|
56459
|
+
const indexPath = path2.join(resolvedPath, idxFile);
|
|
56460
|
+
if (fs2.existsSync(indexPath)) {
|
|
56461
|
+
return indexPath;
|
|
56462
|
+
}
|
|
56463
|
+
}
|
|
56464
|
+
if (fs2.existsSync(resolvedPath)) {
|
|
56465
|
+
return resolvedPath;
|
|
56455
56466
|
}
|
|
56467
|
+
return null;
|
|
56456
56468
|
}
|
|
56457
56469
|
if (tsconfig && tsconfig.paths) {
|
|
56458
|
-
const
|
|
56459
|
-
for (const
|
|
56460
|
-
|
|
56461
|
-
|
|
56462
|
-
|
|
56463
|
-
|
|
56464
|
-
const
|
|
56465
|
-
|
|
56466
|
-
|
|
56467
|
-
|
|
56468
|
-
|
|
56469
|
-
|
|
56470
|
-
|
|
56470
|
+
const paths = tsconfig.paths;
|
|
56471
|
+
for (const [aliasPattern, aliasTargets] of Object.entries(paths)) {
|
|
56472
|
+
const aliasBase = aliasPattern.replace(/\/\*$/, "");
|
|
56473
|
+
if (importPath.startsWith(aliasBase + "/")) {
|
|
56474
|
+
const relativePart = importPath.substring(aliasBase.length);
|
|
56475
|
+
for (const target of aliasTargets) {
|
|
56476
|
+
const targetPath = target.replace(/\/\*$/, "") + relativePart;
|
|
56477
|
+
let resolvedPath = path2.resolve(projectRoot, targetPath);
|
|
56478
|
+
const extensions = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
56479
|
+
for (const ext of extensions) {
|
|
56480
|
+
if (fs2.existsSync(resolvedPath + ext)) {
|
|
56481
|
+
return resolvedPath + ext;
|
|
56482
|
+
}
|
|
56483
|
+
}
|
|
56484
|
+
const indexFiles = ["index.js", "index.jsx", "index.ts", "index.tsx"];
|
|
56485
|
+
for (const idxFile of indexFiles) {
|
|
56486
|
+
const indexPath = path2.join(resolvedPath, idxFile);
|
|
56487
|
+
if (fs2.existsSync(indexPath)) {
|
|
56488
|
+
return indexPath;
|
|
56489
|
+
}
|
|
56471
56490
|
}
|
|
56472
56491
|
}
|
|
56473
56492
|
}
|
|
56474
56493
|
}
|
|
56475
56494
|
}
|
|
56476
56495
|
if (tsconfig && tsconfig.baseUrl && !importPath.startsWith(".")) {
|
|
56477
|
-
|
|
56478
|
-
const
|
|
56479
|
-
|
|
56480
|
-
|
|
56481
|
-
|
|
56482
|
-
|
|
56483
|
-
|
|
56484
|
-
const
|
|
56485
|
-
const
|
|
56486
|
-
|
|
56487
|
-
|
|
56488
|
-
|
|
56489
|
-
if (tsconfig.paths && tsconfig.paths[wildcardPath]) {
|
|
56490
|
-
const pathMappings = tsconfig.paths[wildcardPath];
|
|
56491
|
-
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
56492
|
-
const mappedPath = pathMappings[0].replace("*", "");
|
|
56493
|
-
const realPath2 = path2.resolve(projectRoot, tsconfig.baseUrl || ".", mappedPath);
|
|
56494
|
-
return resolveFile(realPath2);
|
|
56496
|
+
let resolvedPath = path2.resolve(projectRoot, tsconfig.baseUrl, importPath);
|
|
56497
|
+
const extensions = [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"];
|
|
56498
|
+
for (const ext of extensions) {
|
|
56499
|
+
if (fs2.existsSync(resolvedPath + ext)) {
|
|
56500
|
+
return resolvedPath + ext;
|
|
56501
|
+
}
|
|
56502
|
+
}
|
|
56503
|
+
const indexFiles = ["index.js", "index.jsx", "index.ts", "index.tsx"];
|
|
56504
|
+
for (const idxFile of indexFiles) {
|
|
56505
|
+
const indexPath = path2.join(resolvedPath, idxFile);
|
|
56506
|
+
if (fs2.existsSync(indexPath)) {
|
|
56507
|
+
return indexPath;
|
|
56495
56508
|
}
|
|
56496
56509
|
}
|
|
56497
|
-
}
|
|
56498
|
-
if (importPath.startsWith(".")) {
|
|
56499
|
-
const realPath = path2.resolve(path2.dirname(fromFile), importPath);
|
|
56500
|
-
return resolveFile(realPath);
|
|
56501
|
-
}
|
|
56502
|
-
if (!importPath.startsWith(".")) {
|
|
56503
|
-
const realPath = path2.resolve(projectRoot, importPath);
|
|
56504
|
-
return resolveFile(realPath);
|
|
56505
56510
|
}
|
|
56506
56511
|
return null;
|
|
56507
56512
|
}
|
|
56508
|
-
|
|
56509
|
-
|
|
56510
|
-
|
|
56511
|
-
|
|
56512
|
-
|
|
56513
|
-
|
|
56514
|
-
|
|
56515
|
-
|
|
56516
|
-
|
|
56517
|
-
|
|
56513
|
+
module2.exports = { resolveImport };
|
|
56514
|
+
}
|
|
56515
|
+
});
|
|
56516
|
+
|
|
56517
|
+
// src/core/blastRadius.js
|
|
56518
|
+
var require_blastRadius = __commonJS({
|
|
56519
|
+
"src/core/blastRadius.js"(exports2, module2) {
|
|
56520
|
+
function buildReverseDependencyGraph(architectureMap) {
|
|
56521
|
+
const reverseGraph = {};
|
|
56522
|
+
architectureMap.nodes.forEach((node) => {
|
|
56523
|
+
const filePath = node.id;
|
|
56524
|
+
if (!reverseGraph[filePath]) {
|
|
56525
|
+
reverseGraph[filePath] = [];
|
|
56518
56526
|
}
|
|
56527
|
+
});
|
|
56528
|
+
architectureMap.edges.forEach((edge) => {
|
|
56529
|
+
const importingFile = edge.source;
|
|
56530
|
+
const importedFile = edge.target;
|
|
56531
|
+
if (!reverseGraph[importedFile]) {
|
|
56532
|
+
reverseGraph[importedFile] = [];
|
|
56533
|
+
}
|
|
56534
|
+
if (!reverseGraph[importedFile].includes(importingFile)) {
|
|
56535
|
+
reverseGraph[importedFile].push(importingFile);
|
|
56536
|
+
}
|
|
56537
|
+
});
|
|
56538
|
+
return reverseGraph;
|
|
56539
|
+
}
|
|
56540
|
+
function computeBlastRadius(reverseGraph) {
|
|
56541
|
+
const blastRadiusMap = {};
|
|
56542
|
+
for (const [filePath, importingFiles] of Object.entries(reverseGraph)) {
|
|
56543
|
+
blastRadiusMap[filePath] = importingFiles.length;
|
|
56519
56544
|
}
|
|
56520
|
-
|
|
56521
|
-
|
|
56522
|
-
|
|
56523
|
-
|
|
56524
|
-
|
|
56525
|
-
|
|
56545
|
+
return blastRadiusMap;
|
|
56546
|
+
}
|
|
56547
|
+
function findHighestBlastRadius2(blastRadiusMap) {
|
|
56548
|
+
if (Object.keys(blastRadiusMap).length === 0) {
|
|
56549
|
+
return null;
|
|
56550
|
+
}
|
|
56551
|
+
let maxFile = null;
|
|
56552
|
+
let maxRadius = -1;
|
|
56553
|
+
for (const [filePath, radius] of Object.entries(blastRadiusMap)) {
|
|
56554
|
+
if (radius > maxRadius) {
|
|
56555
|
+
maxRadius = radius;
|
|
56556
|
+
maxFile = { file: filePath, blast_radius: radius };
|
|
56526
56557
|
}
|
|
56527
56558
|
}
|
|
56528
|
-
return
|
|
56559
|
+
return maxFile;
|
|
56529
56560
|
}
|
|
56530
|
-
module2.exports = {
|
|
56561
|
+
module2.exports = {
|
|
56562
|
+
buildReverseDependencyGraph,
|
|
56563
|
+
computeBlastRadius,
|
|
56564
|
+
findHighestBlastRadius: findHighestBlastRadius2
|
|
56565
|
+
};
|
|
56531
56566
|
}
|
|
56532
56567
|
});
|
|
56533
56568
|
|
|
@@ -56540,6 +56575,7 @@ var require_scanner = __commonJS({
|
|
|
56540
56575
|
var pluginManager = require_plugin_manager();
|
|
56541
56576
|
var { loadTSConfig } = require_tsconfig_utils();
|
|
56542
56577
|
var { resolveImport } = require_path_resolver();
|
|
56578
|
+
var { buildReverseDependencyGraph, computeBlastRadius } = require_blastRadius();
|
|
56543
56579
|
async function scan(directory) {
|
|
56544
56580
|
const normalize = (p) => p.replace(/\\/g, "/");
|
|
56545
56581
|
const options = {
|
|
@@ -56597,6 +56633,11 @@ var require_scanner = __commonJS({
|
|
|
56597
56633
|
}
|
|
56598
56634
|
});
|
|
56599
56635
|
});
|
|
56636
|
+
const reverseGraph = buildReverseDependencyGraph(architectureMap);
|
|
56637
|
+
const blastRadiusMap = computeBlastRadius(reverseGraph);
|
|
56638
|
+
architectureMap.nodes.forEach((node) => {
|
|
56639
|
+
node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
|
|
56640
|
+
});
|
|
56600
56641
|
return architectureMap;
|
|
56601
56642
|
} catch (err) {
|
|
56602
56643
|
throw err;
|
|
@@ -56623,6 +56664,20 @@ try {
|
|
|
56623
56664
|
}
|
|
56624
56665
|
var CONFIG_FILE = path.join(os.homedir(), ".arcvisionrc");
|
|
56625
56666
|
var API_URL = process.env.ARCVISION_API_URL || "https://arcvisiondev.vercel.app";
|
|
56667
|
+
var { findHighestBlastRadius } = require_blastRadius();
|
|
56668
|
+
function analyzeBlastRadius(architectureMap) {
|
|
56669
|
+
const blastRadiusData = [];
|
|
56670
|
+
architectureMap.nodes.forEach((node) => {
|
|
56671
|
+
blastRadiusData.push({
|
|
56672
|
+
file: node.id,
|
|
56673
|
+
blast_radius: node.metadata.blast_radius || 0
|
|
56674
|
+
});
|
|
56675
|
+
});
|
|
56676
|
+
return findHighestBlastRadius(blastRadiusData.reduce((acc, item) => {
|
|
56677
|
+
acc[item.file] = item.blast_radius;
|
|
56678
|
+
return acc;
|
|
56679
|
+
}, {}));
|
|
56680
|
+
}
|
|
56626
56681
|
function saveToken(token) {
|
|
56627
56682
|
try {
|
|
56628
56683
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ token }));
|
|
@@ -56749,6 +56804,13 @@ program.command("scan").description("Scan the current directory and generate arc
|
|
|
56749
56804
|
try {
|
|
56750
56805
|
const map = await scanner.scan(targetDir);
|
|
56751
56806
|
console.log(chalk.green("Scan complete!"));
|
|
56807
|
+
const blastRadiusInfo = analyzeBlastRadius(map);
|
|
56808
|
+
if (blastRadiusInfo && blastRadiusInfo.blast_radius > 0) {
|
|
56809
|
+
console.log(`
|
|
56810
|
+
\u26A0\uFE0F ${blastRadiusInfo.file} has the highest blast radius (${blastRadiusInfo.blast_radius}). Changes here may affect many parts of the system.`);
|
|
56811
|
+
} else {
|
|
56812
|
+
console.log("\nNo high-risk files detected based on import dependencies.");
|
|
56813
|
+
}
|
|
56752
56814
|
if (options.upload) {
|
|
56753
56815
|
await uploadToDatabase(map);
|
|
56754
56816
|
} else {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Blast Radius Implementation
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
### Files Added/Modified
|
|
6
|
+
1. `src/core/blastRadius.js` - Core blast radius calculation logic
|
|
7
|
+
2. `src/core/scanner.js` - Modified to include blast radius in output
|
|
8
|
+
3. `src/index.js` - Modified to display blast radius insight
|
|
9
|
+
|
|
10
|
+
### Core Functions
|
|
11
|
+
|
|
12
|
+
#### `buildReverseDependencyGraph(architectureMap)`
|
|
13
|
+
- Creates a reverse dependency map where keys are imported files and values are arrays of files that import them
|
|
14
|
+
- Takes the architecture map (with nodes and edges) as input
|
|
15
|
+
- Returns a map: `{ [importedFile]: [importingFile1, importingFile2, ...] }`
|
|
16
|
+
|
|
17
|
+
#### `computeBlastRadius(reverseGraph)`
|
|
18
|
+
- Calculates the blast radius for each file as the count of files that import it
|
|
19
|
+
- Takes the reverse dependency graph as input
|
|
20
|
+
- Returns a map: `{ [filePath]: blastRadiusNumber }`
|
|
21
|
+
|
|
22
|
+
#### `findHighestBlastRadius(blastRadiusMap)`
|
|
23
|
+
- Finds the file with the highest blast radius value
|
|
24
|
+
- Takes the blast radius map as input
|
|
25
|
+
- Returns an object: `{ file: filePath, blast_radius: radius }` or null
|
|
26
|
+
|
|
27
|
+
## Data Flow
|
|
28
|
+
|
|
29
|
+
1. **Scanning Phase**: The scanner builds the architecture map with nodes and edges as before
|
|
30
|
+
2. **Blast Radius Calculation Phase**: After the architecture map is built:
|
|
31
|
+
- Reverse dependency graph is built from the edges
|
|
32
|
+
- Blast radius is computed for each file
|
|
33
|
+
- Blast radius values are added to each node's metadata
|
|
34
|
+
3. **Output Phase**: The CLI prints the architecture map as JSON and then shows the blast radius insight
|
|
35
|
+
|
|
36
|
+
## Data Structure Changes
|
|
37
|
+
|
|
38
|
+
### Node Metadata
|
|
39
|
+
Each node in the architecture map now includes:
|
|
40
|
+
```javascript
|
|
41
|
+
{
|
|
42
|
+
id: "relative/file/path.js",
|
|
43
|
+
type: "file",
|
|
44
|
+
metadata: {
|
|
45
|
+
imports: [...],
|
|
46
|
+
exports: [...],
|
|
47
|
+
functions: [...],
|
|
48
|
+
apiCalls: [...],
|
|
49
|
+
blast_radius: 5 // NEW FIELD
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### CLI Output
|
|
55
|
+
The CLI now prints an additional message after the scan:
|
|
56
|
+
```
|
|
57
|
+
⚠️ src/core/utils.js has the highest blast radius (5). Changes here may affect many parts of the system.
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Algorithm Complexity
|
|
61
|
+
- **Time Complexity**: O(V + E) where V is the number of files and E is the number of import relationships
|
|
62
|
+
- **Space Complexity**: O(V + E) for storing the reverse dependency graph
|
|
63
|
+
|
|
64
|
+
## Error Handling
|
|
65
|
+
- Handles empty repositories gracefully
|
|
66
|
+
- Handles files with no imports or no dependents
|
|
67
|
+
- Maintains backward compatibility with existing functionality
|
|
68
|
+
- Preserves all existing fields in the architecture map
|
|
69
|
+
|
|
70
|
+
## Testing Considerations
|
|
71
|
+
The implementation should be tested with:
|
|
72
|
+
- Empty repositories
|
|
73
|
+
- Repositories with no dependencies
|
|
74
|
+
- Repositories with complex dependency chains
|
|
75
|
+
- Repositories with circular dependencies
|
|
76
|
+
- Large repositories with many files
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Blast Radius Feature
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The Blast Radius feature analyzes your codebase to identify files that are most critical to your application. It calculates how many other files depend on each file, helping you identify high-risk areas where changes could have wide-ranging impacts.
|
|
5
|
+
|
|
6
|
+
## How It Works
|
|
7
|
+
- **Blast Radius Score**: For each file, the blast radius is calculated as the number of files that import it (direct dependencies only).
|
|
8
|
+
- **Reverse Dependency Graph**: The system builds a reverse dependency graph to track which files import each file.
|
|
9
|
+
- **Risk Assessment**: Files with higher blast radius scores are considered higher risk because changes to them could affect many other parts of the system.
|
|
10
|
+
|
|
11
|
+
## Output
|
|
12
|
+
The blast radius score is added to each file's metadata in the architecture map:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"nodes": [
|
|
17
|
+
{
|
|
18
|
+
"id": "src/auth/session.ts",
|
|
19
|
+
"type": "file",
|
|
20
|
+
"metadata": {
|
|
21
|
+
"imports": ["src/db/client.ts", "src/utils/logger.ts"],
|
|
22
|
+
"exports": [],
|
|
23
|
+
"functions": [],
|
|
24
|
+
"apiCalls": [],
|
|
25
|
+
"blast_radius": 2
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"edges": [...]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## CLI Output
|
|
34
|
+
After scanning, the CLI will display a warning for the file with the highest blast radius:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
⚠️ src/auth/session.ts has the highest blast radius (2). Changes here may affect many parts of the system.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Use Cases
|
|
41
|
+
- Identify critical files that require extra care during refactoring
|
|
42
|
+
- Understand the potential impact of code changes
|
|
43
|
+
- Prioritize code review efforts for high-risk files
|
|
44
|
+
- Analyze architectural dependencies in your codebase
|
package/package.json
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blast Radius Analysis
|
|
3
|
+
*
|
|
4
|
+
* This module computes the blast radius for each file in the architecture map.
|
|
5
|
+
* Blast radius is defined as the number of files that import a given file.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds a reverse dependency graph from the architecture map
|
|
10
|
+
* @param {Object} architectureMap - The architecture map with nodes and edges
|
|
11
|
+
* @returns {Object} Reverse dependency map where key is imported file and value is array of importing files
|
|
12
|
+
*/
|
|
13
|
+
function buildReverseDependencyGraph(architectureMap) {
|
|
14
|
+
const reverseGraph = {};
|
|
15
|
+
|
|
16
|
+
// Initialize all files in the reverse graph
|
|
17
|
+
architectureMap.nodes.forEach(node => {
|
|
18
|
+
const filePath = node.id;
|
|
19
|
+
if (!reverseGraph[filePath]) {
|
|
20
|
+
reverseGraph[filePath] = [];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Build reverse dependencies from edges
|
|
25
|
+
architectureMap.edges.forEach(edge => {
|
|
26
|
+
const importingFile = edge.source; // file that imports
|
|
27
|
+
const importedFile = edge.target; // file that is imported
|
|
28
|
+
|
|
29
|
+
if (!reverseGraph[importedFile]) {
|
|
30
|
+
reverseGraph[importedFile] = [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Add the importing file to the list of files that depend on importedFile
|
|
34
|
+
if (!reverseGraph[importedFile].includes(importingFile)) {
|
|
35
|
+
reverseGraph[importedFile].push(importingFile);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return reverseGraph;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Computes blast radius for each file
|
|
44
|
+
* @param {Object} reverseGraph - The reverse dependency graph
|
|
45
|
+
* @returns {Object} Map of file paths to their blast radius (number of files that import them)
|
|
46
|
+
*/
|
|
47
|
+
function computeBlastRadius(reverseGraph) {
|
|
48
|
+
const blastRadiusMap = {};
|
|
49
|
+
|
|
50
|
+
for (const [filePath, importingFiles] of Object.entries(reverseGraph)) {
|
|
51
|
+
blastRadiusMap[filePath] = importingFiles.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return blastRadiusMap;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Finds the file with the highest blast radius
|
|
59
|
+
* @param {Object} blastRadiusMap - Map of file paths to their blast radius
|
|
60
|
+
* @returns {Object|null} Object containing the file path and blast radius, or null if no files
|
|
61
|
+
*/
|
|
62
|
+
function findHighestBlastRadius(blastRadiusMap) {
|
|
63
|
+
if (Object.keys(blastRadiusMap).length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let maxFile = null;
|
|
68
|
+
let maxRadius = -1;
|
|
69
|
+
|
|
70
|
+
for (const [filePath, radius] of Object.entries(blastRadiusMap)) {
|
|
71
|
+
if (radius > maxRadius) {
|
|
72
|
+
maxRadius = radius;
|
|
73
|
+
maxFile = { file: filePath, blast_radius: radius };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return maxFile;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
buildReverseDependencyGraph,
|
|
82
|
+
computeBlastRadius,
|
|
83
|
+
findHighestBlastRadius
|
|
84
|
+
};
|
|
@@ -2,126 +2,109 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Resolve an import path to
|
|
6
|
-
*
|
|
7
|
-
* @param {string}
|
|
5
|
+
* Resolve an import path to its actual file location
|
|
6
|
+
* Handles relative paths, path aliases, barrel files, and implicit extensions
|
|
7
|
+
* @param {string} importPath - The import path from the source code
|
|
8
|
+
* @param {string} importerPath - The path of the file doing the import
|
|
8
9
|
* @param {string} projectRoot - The root directory of the project
|
|
9
|
-
* @param {Object|null} tsconfig -
|
|
10
|
+
* @param {Object|null} tsconfig - Loaded tsconfig with compilerOptions
|
|
10
11
|
* @returns {string|null} The resolved absolute file path or null if not found
|
|
11
12
|
*/
|
|
12
|
-
function resolveImport(importPath,
|
|
13
|
-
//
|
|
14
|
-
if (importPath.startsWith('
|
|
15
|
-
importPath.includes('node_modules/') ||
|
|
16
|
-
importPath.startsWith('http') ||
|
|
17
|
-
importPath.startsWith('//')) {
|
|
13
|
+
function resolveImport(importPath, importerPath, projectRoot, tsconfig) {
|
|
14
|
+
// If it's an absolute path or URL, skip resolution
|
|
15
|
+
if (importPath.startsWith('http://') || importPath.startsWith('https://') || importPath.startsWith('data:')) {
|
|
18
16
|
return null;
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
// Handle
|
|
22
|
-
if (
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
// Handle relative imports
|
|
20
|
+
if (importPath.startsWith('.')) {
|
|
21
|
+
// Resolve relative to importer
|
|
22
|
+
let resolvedPath = path.resolve(path.dirname(importerPath), importPath);
|
|
23
|
+
|
|
24
|
+
// Try with various extensions
|
|
25
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
26
|
+
for (const ext of extensions) {
|
|
27
|
+
if (fs.existsSync(resolvedPath + ext)) {
|
|
28
|
+
return resolvedPath + ext;
|
|
29
|
+
}
|
|
29
30
|
}
|
|
31
|
+
|
|
32
|
+
// Try as directory with index files
|
|
33
|
+
const indexFiles = ['index.js', 'index.jsx', 'index.ts', 'index.tsx'];
|
|
34
|
+
for (const idxFile of indexFiles) {
|
|
35
|
+
const indexPath = path.join(resolvedPath, idxFile);
|
|
36
|
+
if (fs.existsSync(indexPath)) {
|
|
37
|
+
return indexPath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// If original path exists as-is
|
|
42
|
+
if (fs.existsSync(resolvedPath)) {
|
|
43
|
+
return resolvedPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return null;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
// Handle
|
|
49
|
+
// Handle path aliases (like @/components/, ~/utils/, etc.)
|
|
33
50
|
if (tsconfig && tsconfig.paths) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
const paths = tsconfig.paths;
|
|
52
|
+
|
|
53
|
+
// Find matching path alias
|
|
54
|
+
for (const [aliasPattern, aliasTargets] of Object.entries(paths)) {
|
|
55
|
+
// Remove trailing * from pattern
|
|
56
|
+
const aliasBase = aliasPattern.replace(/\/\*$/, '');
|
|
57
|
+
|
|
58
|
+
// If import starts with this alias
|
|
59
|
+
if (importPath.startsWith(aliasBase + '/')) {
|
|
60
|
+
// Get the part after the alias
|
|
61
|
+
const relativePart = importPath.substring(aliasBase.length);
|
|
62
|
+
|
|
63
|
+
// Use the first target path
|
|
64
|
+
for (const target of aliasTargets) {
|
|
65
|
+
// Replace * in target with relative part
|
|
66
|
+
const targetPath = target.replace(/\/\*$/, '') + relativePart;
|
|
67
|
+
let resolvedPath = path.resolve(projectRoot, targetPath);
|
|
68
|
+
|
|
69
|
+
// Try with extensions
|
|
70
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
71
|
+
for (const ext of extensions) {
|
|
72
|
+
if (fs.existsSync(resolvedPath + ext)) {
|
|
73
|
+
return resolvedPath + ext;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Try as directory with index files
|
|
78
|
+
const indexFiles = ['index.js', 'index.jsx', 'index.ts', 'index.tsx'];
|
|
79
|
+
for (const idxFile of indexFiles) {
|
|
80
|
+
const indexPath = path.join(resolvedPath, idxFile);
|
|
81
|
+
if (fs.existsSync(indexPath)) {
|
|
82
|
+
return indexPath;
|
|
83
|
+
}
|
|
46
84
|
}
|
|
47
85
|
}
|
|
48
86
|
}
|
|
49
87
|
}
|
|
50
88
|
}
|
|
51
89
|
|
|
52
|
-
// Handle baseUrl
|
|
90
|
+
// Handle baseUrl if configured
|
|
53
91
|
if (tsconfig && tsconfig.baseUrl && !importPath.startsWith('.')) {
|
|
54
|
-
|
|
55
|
-
const resolved = resolveFile(baseUrlPath);
|
|
56
|
-
if (resolved) return resolved;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Handle @ imports that don't match specific paths mappings
|
|
60
|
-
if (importPath.startsWith('@') && tsconfig && tsconfig.baseUrl) {
|
|
61
|
-
// Remove @ and resolve relative to baseUrl
|
|
62
|
-
const relativePath = importPath.substring(1); // Remove @
|
|
63
|
-
const realPath = path.resolve(projectRoot, tsconfig.baseUrl, relativePath);
|
|
64
|
-
const resolved = resolveFile(realPath);
|
|
65
|
-
if (resolved) return resolved;
|
|
92
|
+
let resolvedPath = path.resolve(projectRoot, tsconfig.baseUrl, importPath);
|
|
66
93
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
73
|
-
const mappedPath = pathMappings[0].replace('*', '');
|
|
74
|
-
const realPath = path.resolve(projectRoot, tsconfig.baseUrl || '.', mappedPath);
|
|
75
|
-
return resolveFile(realPath);
|
|
94
|
+
// Try with extensions
|
|
95
|
+
const extensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
96
|
+
for (const ext of extensions) {
|
|
97
|
+
if (fs.existsSync(resolvedPath + ext)) {
|
|
98
|
+
return resolvedPath + ext;
|
|
76
99
|
}
|
|
77
100
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// For absolute paths within the project that don't match tsconfig paths
|
|
87
|
-
if (!importPath.startsWith('.')) {
|
|
88
|
-
// Try resolving relative to project root
|
|
89
|
-
const realPath = path.resolve(projectRoot, importPath);
|
|
90
|
-
return resolveFile(realPath);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Resolve a potential file path to an actual existing file
|
|
98
|
-
* Handles implicit extensions and barrel files (index files)
|
|
99
|
-
* @param {string} basePath - The base path to resolve
|
|
100
|
-
* @returns {string|null} The resolved file path or null if no file exists
|
|
101
|
-
*/
|
|
102
|
-
function resolveFile(basePath) {
|
|
103
|
-
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
104
|
-
const barrelFiles = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
|
|
105
|
-
|
|
106
|
-
// First, try the exact path
|
|
107
|
-
if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
|
|
108
|
-
return basePath;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Try with extensions
|
|
112
|
-
for (const ext of extensions) {
|
|
113
|
-
const pathWithExt = basePath + ext;
|
|
114
|
-
if (fs.existsSync(pathWithExt) && fs.statSync(pathWithExt).isFile()) {
|
|
115
|
-
return pathWithExt;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Try as a directory with barrel files
|
|
120
|
-
if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
|
|
121
|
-
for (const barrelFile of barrelFiles) {
|
|
122
|
-
const barrelPath = path.join(basePath, barrelFile);
|
|
123
|
-
if (fs.existsSync(barrelPath)) {
|
|
124
|
-
return barrelPath;
|
|
101
|
+
|
|
102
|
+
// Try as directory with index files
|
|
103
|
+
const indexFiles = ['index.js', 'index.jsx', 'index.ts', 'index.tsx'];
|
|
104
|
+
for (const idxFile of indexFiles) {
|
|
105
|
+
const indexPath = path.join(resolvedPath, idxFile);
|
|
106
|
+
if (fs.existsSync(indexPath)) {
|
|
107
|
+
return indexPath;
|
|
125
108
|
}
|
|
126
109
|
}
|
|
127
110
|
}
|
|
@@ -129,4 +112,4 @@ function resolveFile(basePath) {
|
|
|
129
112
|
return null;
|
|
130
113
|
}
|
|
131
114
|
|
|
132
|
-
module.exports = { resolveImport
|
|
115
|
+
module.exports = { resolveImport };
|
package/src/core/scanner.js
CHANGED
|
@@ -4,6 +4,7 @@ const parser = require('./parser');
|
|
|
4
4
|
const pluginManager = require('../plugins/plugin-manager');
|
|
5
5
|
const { loadTSConfig } = require('./tsconfig-utils');
|
|
6
6
|
const { resolveImport } = require('./path-resolver');
|
|
7
|
+
const { buildReverseDependencyGraph, computeBlastRadius } = require('./blastRadius');
|
|
7
8
|
|
|
8
9
|
async function scan(directory) {
|
|
9
10
|
// Normalize helper
|
|
@@ -87,6 +88,15 @@ async function scan(directory) {
|
|
|
87
88
|
});
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
// Calculate blast radius for each file
|
|
92
|
+
const reverseGraph = buildReverseDependencyGraph(architectureMap);
|
|
93
|
+
const blastRadiusMap = computeBlastRadius(reverseGraph);
|
|
94
|
+
|
|
95
|
+
// Add blast_radius to each node
|
|
96
|
+
architectureMap.nodes.forEach(node => {
|
|
97
|
+
node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
|
|
98
|
+
});
|
|
99
|
+
|
|
90
100
|
return architectureMap;
|
|
91
101
|
|
|
92
102
|
} catch (err) {
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,27 @@ try {
|
|
|
24
24
|
const CONFIG_FILE = path.join(os.homedir(), '.arcvisionrc');
|
|
25
25
|
const API_URL = process.env.ARCVISION_API_URL || 'https://arcvisiondev.vercel.app';
|
|
26
26
|
|
|
27
|
+
// Blast radius analysis
|
|
28
|
+
const { findHighestBlastRadius } = require('./core/blastRadius');
|
|
29
|
+
|
|
30
|
+
function analyzeBlastRadius(architectureMap) {
|
|
31
|
+
// Extract blast radius information from nodes
|
|
32
|
+
const blastRadiusData = [];
|
|
33
|
+
|
|
34
|
+
architectureMap.nodes.forEach(node => {
|
|
35
|
+
blastRadiusData.push({
|
|
36
|
+
file: node.id,
|
|
37
|
+
blast_radius: node.metadata.blast_radius || 0
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Find the file with the highest blast radius
|
|
42
|
+
return findHighestBlastRadius(blastRadiusData.reduce((acc, item) => {
|
|
43
|
+
acc[item.file] = item.blast_radius;
|
|
44
|
+
return acc;
|
|
45
|
+
}, {}));
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
function saveToken(token) {
|
|
28
49
|
try {
|
|
29
50
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ token }));
|
|
@@ -181,6 +202,14 @@ program
|
|
|
181
202
|
const map = await scanner.scan(targetDir);
|
|
182
203
|
console.log(chalk.green('Scan complete!'));
|
|
183
204
|
|
|
205
|
+
// Analyze and print blast radius insight
|
|
206
|
+
const blastRadiusInfo = analyzeBlastRadius(map);
|
|
207
|
+
if (blastRadiusInfo && blastRadiusInfo.blast_radius > 0) {
|
|
208
|
+
console.log(`\n⚠️ ${blastRadiusInfo.file} has the highest blast radius (${blastRadiusInfo.blast_radius}). Changes here may affect many parts of the system.`);
|
|
209
|
+
} else {
|
|
210
|
+
console.log('\nNo high-risk files detected based on import dependencies.');
|
|
211
|
+
}
|
|
212
|
+
|
|
184
213
|
// Upload to database if requested
|
|
185
214
|
if (options.upload) {
|
|
186
215
|
await uploadToDatabase(map);
|