arcvision 0.1.0 → 0.1.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/README.md +38 -64
- package/arcvision-0.1.0.tgz +0 -0
- package/dist/index.js +132 -17
- package/package.json +2 -2
- package/src/core/path-resolver.js +120 -0
- package/src/core/scanner.js +28 -19
- package/src/core/tsconfig-utils.js +30 -0
- package/jest.config.json +0 -6
- package/tests/plugins.test.js +0 -49
- package/tests/scanner.test.js +0 -52
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ArcVision CLI
|
|
2
2
|
|
|
3
|
-
Architecture scanner for modern codebases
|
|
3
|
+
Architecture scanner for modern codebases that generates dependency graphs and visualizes your project structure.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,82 +10,56 @@ npm install -g arcvision
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Quick Start
|
|
14
|
+
1. Sign up at the ArcVision dashboard
|
|
15
|
+
2. Create a project and name it
|
|
16
|
+
3. Generate a CLI token
|
|
17
|
+
4. Run: `arcvision link <token>`
|
|
18
|
+
5. Run: `arcvision scan --upload`
|
|
19
|
+
6. Open dashboard to see results
|
|
14
20
|
|
|
21
|
+
### Commands
|
|
22
|
+
|
|
23
|
+
#### Link to Project
|
|
15
24
|
```bash
|
|
16
|
-
arcvision link <
|
|
25
|
+
arcvision link <token>
|
|
17
26
|
```
|
|
27
|
+
Link this CLI to a project via upload token.
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
#### Scan Project
|
|
21
30
|
```bash
|
|
22
|
-
|
|
23
|
-
arcvision scan
|
|
24
|
-
|
|
25
|
-
# Scan specific directory
|
|
26
|
-
arcvision scan path/to/directory
|
|
27
|
-
|
|
28
|
-
# Scan and upload to dashboard
|
|
29
|
-
arcvision scan --upload
|
|
31
|
+
arcvision scan [directory] [options]
|
|
30
32
|
```
|
|
33
|
+
Scan the current directory and generate architecture map.
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- `arcvision link <token>` - Link the CLI to a project using an upload token
|
|
35
|
-
- `arcvision scan [directory]` - Scan a directory and generate architecture map
|
|
36
|
-
- `-u, --upload` - Upload the scan results to the dashboard
|
|
35
|
+
Options:
|
|
36
|
+
- `-u, --upload`: Upload to database
|
|
37
37
|
|
|
38
38
|
## Features
|
|
39
39
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
## Plugin System
|
|
47
|
-
|
|
48
|
-
The CLI includes a plugin system that enhances code analysis:
|
|
49
|
-
|
|
50
|
-
- **React Plugin**: Detects React components and hooks
|
|
51
|
-
- **Express Plugin**: Identifies Express routes and endpoints
|
|
52
|
-
- **Custom Plugins**: Extend functionality with your own plugins
|
|
53
|
-
|
|
54
|
-
## Development
|
|
55
|
-
|
|
56
|
-
To build the CLI from source:
|
|
40
|
+
- **Modern Import Resolution**: Handles path aliases (`@/components/*`), barrel files (`./utils` → `./utils/index.ts`), and directory imports
|
|
41
|
+
- **AST-based Parsing**: Uses Babel parser for accurate import detection
|
|
42
|
+
- **Multi-framework Support**: Works with React, Next.js, and other modern JavaScript frameworks
|
|
43
|
+
- **Dependency Graph Generation**: Creates comprehensive node-edge relationships
|
|
44
|
+
- **Cloud Integration**: Uploads results to ArcVision dashboard for visualization
|
|
57
45
|
|
|
58
|
-
|
|
59
|
-
# Clone the repository
|
|
60
|
-
git clone <repository-url>
|
|
61
|
-
|
|
62
|
-
# Navigate to CLI directory
|
|
63
|
-
cd cli
|
|
64
|
-
|
|
65
|
-
# Install dependencies
|
|
66
|
-
npm install
|
|
67
|
-
|
|
68
|
-
# Build the CLI
|
|
69
|
-
npm run build
|
|
70
|
-
|
|
71
|
-
# Run in development mode
|
|
72
|
-
npm run dev
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Configuration
|
|
76
|
-
|
|
77
|
-
The CLI stores authentication tokens in `~/.arcvisionrc` in your home directory. This file is created automatically when you run `arcvision link <token>`.
|
|
46
|
+
## Supported Import Patterns
|
|
78
47
|
|
|
79
|
-
|
|
48
|
+
- Relative imports: `./utils`, `../components`
|
|
49
|
+
- Path aliases: `@/components/Button`, `~/lib/utils`
|
|
50
|
+
- Barrel files: `./utils` resolves to `./utils/index.ts(x)`
|
|
51
|
+
- Directory imports: `@/utils` resolves to `src/utils/index.ts(x)`
|
|
52
|
+
- Implicit extensions: `.ts`, `.tsx`, `.js`, `.jsx`
|
|
80
53
|
|
|
81
|
-
|
|
54
|
+
## Architecture
|
|
82
55
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
56
|
+
The ArcVision CLI runs locally on your machine and:
|
|
57
|
+
1. Scans your project files using AST parsing
|
|
58
|
+
2. Detects import relationships between files
|
|
59
|
+
3. Resolves path aliases using tsconfig.json/jsconfig.json
|
|
60
|
+
4. Generates a dependency graph in JSON format
|
|
61
|
+
5. Uploads the graph to the ArcVision dashboard via API
|
|
86
62
|
|
|
87
|
-
##
|
|
63
|
+
## License
|
|
88
64
|
|
|
89
|
-
|
|
90
|
-
- **Network errors**: Check your internet connection and API endpoint accessibility
|
|
91
|
-
- **Parse errors**: The scanner skips TypeScript declaration files (.d.ts) by default
|
|
65
|
+
MIT
|
package/arcvision-0.1.0.tgz
CHANGED
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -56388,6 +56388,118 @@ var require_plugin_manager = __commonJS({
|
|
|
56388
56388
|
}
|
|
56389
56389
|
});
|
|
56390
56390
|
|
|
56391
|
+
// src/core/tsconfig-utils.js
|
|
56392
|
+
var require_tsconfig_utils = __commonJS({
|
|
56393
|
+
"src/core/tsconfig-utils.js"(exports2, module2) {
|
|
56394
|
+
var fs2 = require("fs");
|
|
56395
|
+
var path2 = require("path");
|
|
56396
|
+
function loadTSConfig(projectRoot) {
|
|
56397
|
+
const tsconfigPaths = [
|
|
56398
|
+
path2.join(projectRoot, "tsconfig.json"),
|
|
56399
|
+
path2.join(projectRoot, "jsconfig.json")
|
|
56400
|
+
];
|
|
56401
|
+
for (const tsconfigPath of tsconfigPaths) {
|
|
56402
|
+
if (fs2.existsSync(tsconfigPath)) {
|
|
56403
|
+
try {
|
|
56404
|
+
const raw = JSON.parse(fs2.readFileSync(tsconfigPath, "utf-8"));
|
|
56405
|
+
return raw.compilerOptions || {};
|
|
56406
|
+
} catch (error) {
|
|
56407
|
+
console.warn(`Warning: Could not parse ${tsconfigPath}:`, error.message);
|
|
56408
|
+
return null;
|
|
56409
|
+
}
|
|
56410
|
+
}
|
|
56411
|
+
}
|
|
56412
|
+
return null;
|
|
56413
|
+
}
|
|
56414
|
+
module2.exports = { loadTSConfig };
|
|
56415
|
+
}
|
|
56416
|
+
});
|
|
56417
|
+
|
|
56418
|
+
// src/core/path-resolver.js
|
|
56419
|
+
var require_path_resolver = __commonJS({
|
|
56420
|
+
"src/core/path-resolver.js"(exports2, module2) {
|
|
56421
|
+
var fs2 = require("fs");
|
|
56422
|
+
var path2 = require("path");
|
|
56423
|
+
function resolveImport(importPath, fromFile, projectRoot, tsconfig) {
|
|
56424
|
+
if (importPath.startsWith("node_modules/") || importPath.includes("node_modules/") || importPath.startsWith("http") || importPath.startsWith("//")) {
|
|
56425
|
+
return null;
|
|
56426
|
+
}
|
|
56427
|
+
if (tsconfig && tsconfig.paths && importPath in tsconfig.paths) {
|
|
56428
|
+
const pathMappings = tsconfig.paths[importPath];
|
|
56429
|
+
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
56430
|
+
const mappedPath = pathMappings[0].replace("*", importPath.substring(0, importPath.lastIndexOf("/")));
|
|
56431
|
+
const realPath = path2.resolve(projectRoot, tsconfig.baseUrl || ".", mappedPath);
|
|
56432
|
+
return resolveFile(realPath);
|
|
56433
|
+
}
|
|
56434
|
+
}
|
|
56435
|
+
if (tsconfig && tsconfig.paths) {
|
|
56436
|
+
const pathKeys = Object.keys(tsconfig.paths);
|
|
56437
|
+
for (const pathKey of pathKeys) {
|
|
56438
|
+
if (pathKey.endsWith("*")) {
|
|
56439
|
+
const prefix = pathKey.slice(0, -1);
|
|
56440
|
+
if (importPath.startsWith(prefix)) {
|
|
56441
|
+
const suffix = importPath.substring(prefix.length);
|
|
56442
|
+
const pathMappings = tsconfig.paths[pathKey];
|
|
56443
|
+
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
56444
|
+
const mappedPath = pathMappings[0].replace("*", suffix);
|
|
56445
|
+
const realPath = path2.resolve(projectRoot, tsconfig.baseUrl || ".", mappedPath);
|
|
56446
|
+
const resolved = resolveFile(realPath);
|
|
56447
|
+
if (resolved)
|
|
56448
|
+
return resolved;
|
|
56449
|
+
}
|
|
56450
|
+
}
|
|
56451
|
+
}
|
|
56452
|
+
}
|
|
56453
|
+
}
|
|
56454
|
+
if (tsconfig && tsconfig.baseUrl && !importPath.startsWith(".")) {
|
|
56455
|
+
const baseUrlPath = path2.resolve(projectRoot, tsconfig.baseUrl, importPath);
|
|
56456
|
+
const resolved = resolveFile(baseUrlPath);
|
|
56457
|
+
if (resolved)
|
|
56458
|
+
return resolved;
|
|
56459
|
+
}
|
|
56460
|
+
if (importPath.startsWith("@") && tsconfig && tsconfig.baseUrl) {
|
|
56461
|
+
const relativePath = importPath.substring(1);
|
|
56462
|
+
const realPath = path2.resolve(projectRoot, tsconfig.baseUrl, relativePath);
|
|
56463
|
+
const resolved = resolveFile(realPath);
|
|
56464
|
+
if (resolved)
|
|
56465
|
+
return resolved;
|
|
56466
|
+
}
|
|
56467
|
+
if (importPath.startsWith(".")) {
|
|
56468
|
+
const realPath = path2.resolve(path2.dirname(fromFile), importPath);
|
|
56469
|
+
return resolveFile(realPath);
|
|
56470
|
+
}
|
|
56471
|
+
if (!importPath.startsWith(".")) {
|
|
56472
|
+
const realPath = path2.resolve(projectRoot, importPath);
|
|
56473
|
+
return resolveFile(realPath);
|
|
56474
|
+
}
|
|
56475
|
+
return null;
|
|
56476
|
+
}
|
|
56477
|
+
function resolveFile(basePath) {
|
|
56478
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
56479
|
+
const barrelFiles = ["index.ts", "index.tsx", "index.js", "index.jsx"];
|
|
56480
|
+
if (fs2.existsSync(basePath) && fs2.statSync(basePath).isFile()) {
|
|
56481
|
+
return basePath;
|
|
56482
|
+
}
|
|
56483
|
+
for (const ext of extensions) {
|
|
56484
|
+
const pathWithExt = basePath + ext;
|
|
56485
|
+
if (fs2.existsSync(pathWithExt) && fs2.statSync(pathWithExt).isFile()) {
|
|
56486
|
+
return pathWithExt;
|
|
56487
|
+
}
|
|
56488
|
+
}
|
|
56489
|
+
if (fs2.existsSync(basePath) && fs2.statSync(basePath).isDirectory()) {
|
|
56490
|
+
for (const barrelFile of barrelFiles) {
|
|
56491
|
+
const barrelPath = path2.join(basePath, barrelFile);
|
|
56492
|
+
if (fs2.existsSync(barrelPath)) {
|
|
56493
|
+
return barrelPath;
|
|
56494
|
+
}
|
|
56495
|
+
}
|
|
56496
|
+
}
|
|
56497
|
+
return null;
|
|
56498
|
+
}
|
|
56499
|
+
module2.exports = { resolveImport, resolveFile };
|
|
56500
|
+
}
|
|
56501
|
+
});
|
|
56502
|
+
|
|
56391
56503
|
// src/core/scanner.js
|
|
56392
56504
|
var require_scanner = __commonJS({
|
|
56393
56505
|
"src/core/scanner.js"(exports2, module2) {
|
|
@@ -56395,6 +56507,8 @@ var require_scanner = __commonJS({
|
|
|
56395
56507
|
var path2 = require("path");
|
|
56396
56508
|
var parser = require_parser();
|
|
56397
56509
|
var pluginManager = require_plugin_manager();
|
|
56510
|
+
var { loadTSConfig } = require_tsconfig_utils();
|
|
56511
|
+
var { resolveImport } = require_path_resolver();
|
|
56398
56512
|
async function scan(directory) {
|
|
56399
56513
|
const options = {
|
|
56400
56514
|
ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
|
|
@@ -56427,25 +56541,26 @@ var require_scanner = __commonJS({
|
|
|
56427
56541
|
}
|
|
56428
56542
|
}
|
|
56429
56543
|
const normalize = (p) => p.replace(/\\/g, "/");
|
|
56430
|
-
const
|
|
56431
|
-
|
|
56544
|
+
const tsconfig = loadTSConfig(directory);
|
|
56545
|
+
const normalizedFileMap = /* @__PURE__ */ new Map();
|
|
56546
|
+
architectureMap.nodes.forEach((n) => normalizedFileMap.set(normalize(n.id), n.id));
|
|
56432
56547
|
architectureMap.nodes.forEach((node) => {
|
|
56433
56548
|
node.metadata.imports.forEach((imp) => {
|
|
56434
|
-
|
|
56435
|
-
|
|
56436
|
-
|
|
56437
|
-
|
|
56438
|
-
|
|
56439
|
-
|
|
56440
|
-
|
|
56441
|
-
|
|
56442
|
-
|
|
56443
|
-
|
|
56444
|
-
|
|
56445
|
-
|
|
56446
|
-
|
|
56447
|
-
|
|
56448
|
-
}
|
|
56549
|
+
const resolvedPath = resolveImport(
|
|
56550
|
+
imp.source,
|
|
56551
|
+
path2.join(directory, node.id),
|
|
56552
|
+
directory,
|
|
56553
|
+
tsconfig
|
|
56554
|
+
);
|
|
56555
|
+
if (resolvedPath) {
|
|
56556
|
+
const relativeResolvedPath = path2.relative(directory, resolvedPath);
|
|
56557
|
+
const normalizedResolvedPath = normalize(relativeResolvedPath);
|
|
56558
|
+
if (normalizedFileMap.has(normalizedResolvedPath)) {
|
|
56559
|
+
architectureMap.edges.push({
|
|
56560
|
+
source: normalize(node.id),
|
|
56561
|
+
target: normalizedResolvedPath,
|
|
56562
|
+
type: "import"
|
|
56563
|
+
});
|
|
56449
56564
|
}
|
|
56450
56565
|
}
|
|
56451
56566
|
});
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve an import path to an actual file on disk
|
|
6
|
+
* @param {string} importPath - The import string from the code (e.g., '@/components/Button', './utils')
|
|
7
|
+
* @param {string} fromFile - The file that contains the import
|
|
8
|
+
* @param {string} projectRoot - The root directory of the project
|
|
9
|
+
* @param {Object|null} tsconfig - The compilerOptions from tsconfig.json
|
|
10
|
+
* @returns {string|null} The resolved absolute file path or null if not found
|
|
11
|
+
*/
|
|
12
|
+
function resolveImport(importPath, fromFile, projectRoot, tsconfig) {
|
|
13
|
+
// Skip external packages and node_modules
|
|
14
|
+
if (importPath.startsWith('node_modules/') ||
|
|
15
|
+
importPath.includes('node_modules/') ||
|
|
16
|
+
importPath.startsWith('http') ||
|
|
17
|
+
importPath.startsWith('//')) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle path aliases (e.g., '@/components/Button', '@/lib/utils')
|
|
22
|
+
if (tsconfig && tsconfig.paths && importPath in tsconfig.paths) {
|
|
23
|
+
// Direct path mapping
|
|
24
|
+
const pathMappings = tsconfig.paths[importPath];
|
|
25
|
+
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
26
|
+
const mappedPath = pathMappings[0].replace('*', importPath.substring(0, importPath.lastIndexOf('/')));
|
|
27
|
+
const realPath = path.resolve(projectRoot, tsconfig.baseUrl || '.', mappedPath);
|
|
28
|
+
return resolveFile(realPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle wildcard path aliases (e.g., '@/components/*' -> './src/components/*')
|
|
33
|
+
if (tsconfig && tsconfig.paths) {
|
|
34
|
+
const pathKeys = Object.keys(tsconfig.paths);
|
|
35
|
+
for (const pathKey of pathKeys) {
|
|
36
|
+
if (pathKey.endsWith('*')) {
|
|
37
|
+
const prefix = pathKey.slice(0, -1); // Remove the '*'
|
|
38
|
+
if (importPath.startsWith(prefix)) {
|
|
39
|
+
const suffix = importPath.substring(prefix.length);
|
|
40
|
+
const pathMappings = tsconfig.paths[pathKey];
|
|
41
|
+
if (Array.isArray(pathMappings) && pathMappings.length > 0) {
|
|
42
|
+
const mappedPath = pathMappings[0].replace('*', suffix);
|
|
43
|
+
const realPath = path.resolve(projectRoot, tsconfig.baseUrl || '.', mappedPath);
|
|
44
|
+
const resolved = resolveFile(realPath);
|
|
45
|
+
if (resolved) return resolved;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle baseUrl resolution (e.g., 'components/Button' -> './src/components/Button')
|
|
53
|
+
if (tsconfig && tsconfig.baseUrl && !importPath.startsWith('.')) {
|
|
54
|
+
const baseUrlPath = path.resolve(projectRoot, tsconfig.baseUrl, importPath);
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle relative paths (e.g., './utils', '../components')
|
|
69
|
+
if (importPath.startsWith('.')) {
|
|
70
|
+
const realPath = path.resolve(path.dirname(fromFile), importPath);
|
|
71
|
+
return resolveFile(realPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For absolute paths within the project that don't match tsconfig paths
|
|
75
|
+
if (!importPath.startsWith('.')) {
|
|
76
|
+
// Try resolving relative to project root
|
|
77
|
+
const realPath = path.resolve(projectRoot, importPath);
|
|
78
|
+
return resolveFile(realPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a potential file path to an actual existing file
|
|
86
|
+
* Handles implicit extensions and barrel files (index files)
|
|
87
|
+
* @param {string} basePath - The base path to resolve
|
|
88
|
+
* @returns {string|null} The resolved file path or null if no file exists
|
|
89
|
+
*/
|
|
90
|
+
function resolveFile(basePath) {
|
|
91
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
92
|
+
const barrelFiles = ['index.ts', 'index.tsx', 'index.js', 'index.jsx'];
|
|
93
|
+
|
|
94
|
+
// First, try the exact path
|
|
95
|
+
if (fs.existsSync(basePath) && fs.statSync(basePath).isFile()) {
|
|
96
|
+
return basePath;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Try with extensions
|
|
100
|
+
for (const ext of extensions) {
|
|
101
|
+
const pathWithExt = basePath + ext;
|
|
102
|
+
if (fs.existsSync(pathWithExt) && fs.statSync(pathWithExt).isFile()) {
|
|
103
|
+
return pathWithExt;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Try as a directory with barrel files
|
|
108
|
+
if (fs.existsSync(basePath) && fs.statSync(basePath).isDirectory()) {
|
|
109
|
+
for (const barrelFile of barrelFiles) {
|
|
110
|
+
const barrelPath = path.join(basePath, barrelFile);
|
|
111
|
+
if (fs.existsSync(barrelPath)) {
|
|
112
|
+
return barrelPath;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { resolveImport, resolveFile };
|
package/src/core/scanner.js
CHANGED
|
@@ -2,6 +2,8 @@ const { glob } = require('glob');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const parser = require('./parser');
|
|
4
4
|
const pluginManager = require('../plugins/plugin-manager');
|
|
5
|
+
const { loadTSConfig } = require('./tsconfig-utils');
|
|
6
|
+
const { resolveImport } = require('./path-resolver');
|
|
5
7
|
|
|
6
8
|
async function scan(directory) {
|
|
7
9
|
const options = {
|
|
@@ -50,27 +52,34 @@ async function scan(directory) {
|
|
|
50
52
|
// Normalize helper
|
|
51
53
|
const normalize = p => p.replace(/\\/g, '/');
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// Load tsconfig for path resolution
|
|
56
|
+
const tsconfig = loadTSConfig(directory);
|
|
57
|
+
|
|
58
|
+
const normalizedFileMap = new Map();
|
|
59
|
+
architectureMap.nodes.forEach(n => normalizedFileMap.set(normalize(n.id), n.id));
|
|
55
60
|
|
|
56
61
|
architectureMap.nodes.forEach(node => {
|
|
57
62
|
node.metadata.imports.forEach(imp => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
// Resolve the import path using the new resolver
|
|
64
|
+
const resolvedPath = resolveImport(
|
|
65
|
+
imp.source,
|
|
66
|
+
path.join(directory, node.id),
|
|
67
|
+
directory,
|
|
68
|
+
tsconfig
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (resolvedPath) {
|
|
72
|
+
// Convert resolved absolute path back to relative path
|
|
73
|
+
const relativeResolvedPath = path.relative(directory, resolvedPath);
|
|
74
|
+
const normalizedResolvedPath = normalize(relativeResolvedPath);
|
|
75
|
+
|
|
76
|
+
// Check if the resolved file exists in our scanned files
|
|
77
|
+
if (normalizedFileMap.has(normalizedResolvedPath)) {
|
|
78
|
+
architectureMap.edges.push({
|
|
79
|
+
source: normalize(node.id),
|
|
80
|
+
target: normalizedResolvedPath,
|
|
81
|
+
type: 'import'
|
|
82
|
+
});
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
});
|
|
@@ -83,4 +92,4 @@ async function scan(directory) {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
module.exports = { scan };
|
|
95
|
+
module.exports = { scan };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Load and parse tsconfig.json from project root
|
|
6
|
+
* @param {string} projectRoot - The root directory of the project
|
|
7
|
+
* @returns {Object|null} The compilerOptions from tsconfig.json or null if not found
|
|
8
|
+
*/
|
|
9
|
+
function loadTSConfig(projectRoot) {
|
|
10
|
+
const tsconfigPaths = [
|
|
11
|
+
path.join(projectRoot, 'tsconfig.json'),
|
|
12
|
+
path.join(projectRoot, 'jsconfig.json')
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
for (const tsconfigPath of tsconfigPaths) {
|
|
16
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
|
|
19
|
+
return raw.compilerOptions || {};
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.warn(`Warning: Could not parse ${tsconfigPath}:`, error.message);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { loadTSConfig };
|
package/jest.config.json
DELETED
package/tests/plugins.test.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
const pluginManager = require('./src/plugins/plugin-manager');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
describe('Plugin System', () => {
|
|
5
|
-
test('should register a plugin', () => {
|
|
6
|
-
const testPlugin = {
|
|
7
|
-
name: 'test-plugin',
|
|
8
|
-
process: async (filePath, metadata) => {
|
|
9
|
-
return { testField: 'test-value' };
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
pluginManager.register(testPlugin);
|
|
14
|
-
expect(pluginManager.plugins.length).toBeGreaterThan(0);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('should process file with plugin', async () => {
|
|
18
|
-
const testPlugin = {
|
|
19
|
-
name: 'test-enhancer',
|
|
20
|
-
process: async (filePath, metadata) => {
|
|
21
|
-
return { enhanced: true };
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
pluginManager.register(testPlugin);
|
|
26
|
-
|
|
27
|
-
const metadata = { id: 'test.js', imports: [] };
|
|
28
|
-
const result = await pluginManager.processFile('test.js', metadata);
|
|
29
|
-
|
|
30
|
-
expect(result.enhanced).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('should handle plugin errors gracefully', async () => {
|
|
34
|
-
const faultyPlugin = {
|
|
35
|
-
name: 'faulty-plugin',
|
|
36
|
-
process: async () => {
|
|
37
|
-
throw new Error('Plugin error');
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
pluginManager.register(faultyPlugin);
|
|
42
|
-
|
|
43
|
-
const metadata = { id: 'test.js' };
|
|
44
|
-
const result = await pluginManager.processFile('test.js', metadata);
|
|
45
|
-
|
|
46
|
-
// Should not crash
|
|
47
|
-
expect(result).toBeDefined();
|
|
48
|
-
});
|
|
49
|
-
});
|
package/tests/scanner.test.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
const scanner = require('./src/core/scanner');
|
|
2
|
-
const parser = require('./src/core/parser');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
|
|
5
|
-
describe('ArcVision Scanner', () => {
|
|
6
|
-
test('should parse a simple JavaScript file', () => {
|
|
7
|
-
const testFile = path.join(__dirname, '../example-project/utils.js');
|
|
8
|
-
const metadata = parser.parseFile(testFile);
|
|
9
|
-
|
|
10
|
-
expect(metadata).toBeDefined();
|
|
11
|
-
expect(metadata.exports).toContain('helper');
|
|
12
|
-
expect(metadata.functions).toBeDefined();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('should scan a directory and return architecture map', async () => {
|
|
16
|
-
const testDir = path.join(__dirname, '../example-project');
|
|
17
|
-
const map = await scanner.scan(testDir);
|
|
18
|
-
|
|
19
|
-
expect(map).toBeDefined();
|
|
20
|
-
expect(map.nodes).toBeDefined();
|
|
21
|
-
expect(map.edges).toBeDefined();
|
|
22
|
-
expect(map.nodes.length).toBeGreaterThan(0);
|
|
23
|
-
}, 30000);
|
|
24
|
-
|
|
25
|
-
test('should detect imports and create edges', async () => {
|
|
26
|
-
const testDir = path.join(__dirname, '../example-project');
|
|
27
|
-
const map = await scanner.scan(testDir);
|
|
28
|
-
|
|
29
|
-
expect(map.edges.length).toBeGreaterThan(0);
|
|
30
|
-
const hasImportEdge = map.edges.some(e => e.type === 'import');
|
|
31
|
-
expect(hasImportEdge).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('should detect API calls', async () => {
|
|
35
|
-
const testDir = path.join(__dirname, '../example-project');
|
|
36
|
-
const map = await scanner.scan(testDir);
|
|
37
|
-
|
|
38
|
-
const apiFile = map.nodes.find(n => n.id.includes('api.js'));
|
|
39
|
-
expect(apiFile).toBeDefined();
|
|
40
|
-
expect(apiFile.metadata.apiCalls).toBeDefined();
|
|
41
|
-
expect(apiFile.metadata.apiCalls.length).toBeGreaterThan(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('should handle circular dependencies', async () => {
|
|
45
|
-
// This is a basic test - in real scenarios we'd create files with circular deps
|
|
46
|
-
const testDir = path.join(__dirname, '../example-project');
|
|
47
|
-
const map = await scanner.scan(testDir);
|
|
48
|
-
|
|
49
|
-
// Should not crash
|
|
50
|
-
expect(map).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
});
|