coderaph 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/README.md +126 -0
- package/dist/cli/analyzer.d.ts +13 -0
- package/dist/cli/analyzer.js +68 -0
- package/dist/cli/file-graph.d.ts +6 -0
- package/dist/cli/file-graph.js +32 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +69 -0
- package/dist/cli/server.d.ts +1 -0
- package/dist/cli/server.js +68 -0
- package/dist/cli/symbol-graph.d.ts +6 -0
- package/dist/cli/symbol-graph.js +112 -0
- package/dist/types/graph.d.ts +22 -0
- package/dist/types/graph.js +1 -0
- package/package.json +25 -0
- package/src/web/app.js +790 -0
- package/src/web/controls.js +732 -0
- package/src/web/graph.js +245 -0
- package/src/web/index.html +93 -0
- package/src/web/style.css +102 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Coderaph
|
|
2
|
+
|
|
3
|
+
TypeScript 코드베이스의 의존성 관계를 인터랙티브 3D 그래프로 시각화하는 CLI 도구.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **파일/심볼 수준 의존성 분석** - `ts-morph` 기반 AST 분석으로 import, 참조 관계를 추출
|
|
12
|
+
- **3D Force-Directed Graph** - Three.js 기반 물리 시뮬레이션 레이아웃
|
|
13
|
+
- **2D/3D 전환** - 평면 뷰와 입체 뷰 간 자유 전환
|
|
14
|
+
- **방향 화살표** - 엣지에 방향 표시로 의존성 흐름 파악
|
|
15
|
+
- **검색 & 자동완성** - 심볼/파일명 검색, 결과 내비게이션(prev/next)
|
|
16
|
+
- **필터링** - 심볼 종류(class, function, interface 등) 및 경로 기반 필터
|
|
17
|
+
- **그룹 시각화** - `path:`, `file:`, `kind:` 쿼리로 노드 그룹을 ConvexHull로 표시
|
|
18
|
+
- **의존성 체인** - 특정 노드의 상위/하위 의존성을 재귀적으로 추적
|
|
19
|
+
- **다크/라이트 테마** - 설정 자동 저장(localStorage)
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# 설치
|
|
25
|
+
npm install -g coderaph
|
|
26
|
+
|
|
27
|
+
# 또는 npx로 바로 실행
|
|
28
|
+
npx coderaph analyze ./my-project -o graph.json
|
|
29
|
+
npx coderaph view graph.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### 프로젝트 분석
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
coderaph analyze <project-path> [options]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
| Option | Description | Default |
|
|
41
|
+
|--------|-------------|---------|
|
|
42
|
+
| `-o, --output <path>` | 출력 JSON 파일 경로 | `coderaph-output.json` |
|
|
43
|
+
| `--include <patterns...>` | 포함할 glob 패턴 | tsconfig 전체 |
|
|
44
|
+
| `--exclude <patterns...>` | 제외할 glob 패턴 | - |
|
|
45
|
+
|
|
46
|
+
`tsconfig.json`이 있는 TypeScript 프로젝트 디렉토리를 지정합니다. `node_modules`와 `.d.ts` 파일은 자동으로 제외됩니다.
|
|
47
|
+
|
|
48
|
+
### 웹 뷰어 실행
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
coderaph view <json-path> [--port 3000]
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
브라우저에서 인터랙티브 3D 그래프를 확인할 수 있습니다.
|
|
55
|
+
|
|
56
|
+
## Web Viewer Controls
|
|
57
|
+
|
|
58
|
+
| Action | Description |
|
|
59
|
+
|--------|-------------|
|
|
60
|
+
| 마우스 드래그 | 3D: 회전 / 2D: 패닝 |
|
|
61
|
+
| 스크롤 | 줌 인/아웃 |
|
|
62
|
+
| 노드 클릭 | 정보 패널 표시 |
|
|
63
|
+
| 노드 더블클릭 | 의존성 체인 필터링 |
|
|
64
|
+
| ESC | 필터 해제 |
|
|
65
|
+
|
|
66
|
+
### Settings
|
|
67
|
+
|
|
68
|
+
- **Theme**: 다크/라이트 모드
|
|
69
|
+
- **Dimension**: 2D/3D 전환
|
|
70
|
+
- **Node Size / Edge Thickness / Node Distance**: 레이아웃 조정
|
|
71
|
+
- **Show Labels / Show Arrows**: 라벨 및 방향 화살표 토글
|
|
72
|
+
|
|
73
|
+
### Group
|
|
74
|
+
|
|
75
|
+
`path:`, `file:`, `kind:` 접두사를 사용하여 노드를 그룹화하고 ConvexHull로 시각화할 수 있습니다.
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
path:src/cli → src/cli 경로의 파일 그룹
|
|
79
|
+
file:graph → 파일명에 graph를 포함하는 노드 그룹
|
|
80
|
+
kind:class → 모든 class 심볼 그룹
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Project Structure
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
coderaph/
|
|
87
|
+
├── src/
|
|
88
|
+
│ ├── cli/
|
|
89
|
+
│ │ ├── index.ts # CLI 진입점
|
|
90
|
+
│ │ ├── analyzer.ts # ts-morph AST 분석
|
|
91
|
+
│ │ ├── file-graph.ts # 파일 수준 그래프 생성
|
|
92
|
+
│ │ ├── symbol-graph.ts # 심볼 수준 그래프 생성
|
|
93
|
+
│ │ └── server.ts # 웹 뷰어 HTTP 서버
|
|
94
|
+
│ ├── web/
|
|
95
|
+
│ │ ├── index.html
|
|
96
|
+
│ │ ├── app.js # Three.js 3D 렌더링
|
|
97
|
+
│ │ ├── graph.js # Force-directed 레이아웃 엔진
|
|
98
|
+
│ │ ├── controls.js # UI 컨트롤
|
|
99
|
+
│ │ └── style.css
|
|
100
|
+
│ └── types/
|
|
101
|
+
│ └── graph.ts # JSON 스키마 타입
|
|
102
|
+
├── package.json
|
|
103
|
+
└── tsconfig.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Tech Stack
|
|
107
|
+
|
|
108
|
+
- **CLI**: TypeScript, [ts-morph](https://ts-morph.com/), [Commander.js](https://github.com/tj/commander.js)
|
|
109
|
+
- **Web**: Vanilla JS, [Three.js](https://threejs.org/) (CDN ESM)
|
|
110
|
+
- **Build**: `tsc` (CLI만 컴파일, 웹은 빌드 없이 서빙)
|
|
111
|
+
|
|
112
|
+
## Development
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
git clone https://github.com/yoonhoGo/coderaph.git
|
|
116
|
+
cd coderaph
|
|
117
|
+
npm install
|
|
118
|
+
npm run build
|
|
119
|
+
|
|
120
|
+
# 개발 모드 (watch)
|
|
121
|
+
npm run dev
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Project, SourceFile } from 'ts-morph';
|
|
2
|
+
export interface AnalyzerOptions {
|
|
3
|
+
projectPath: string;
|
|
4
|
+
include?: string[];
|
|
5
|
+
exclude?: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Loads a TypeScript project and returns filtered source files.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadProject(options: AnalyzerOptions): {
|
|
11
|
+
project: Project;
|
|
12
|
+
sourceFiles: SourceFile[];
|
|
13
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { Project } from 'ts-morph';
|
|
4
|
+
/**
|
|
5
|
+
* Simple glob matcher that supports `*` as a wildcard for any characters.
|
|
6
|
+
* If the pattern contains no `*`, it falls back to substring matching.
|
|
7
|
+
*/
|
|
8
|
+
function matchGlob(filePath, pattern) {
|
|
9
|
+
if (!pattern.includes('*')) {
|
|
10
|
+
return filePath.includes(pattern);
|
|
11
|
+
}
|
|
12
|
+
// Convert glob pattern to regex: escape special chars, replace * with .*
|
|
13
|
+
const escaped = pattern
|
|
14
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
15
|
+
.replace(/\*/g, '.*');
|
|
16
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
17
|
+
return regex.test(filePath);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Loads a TypeScript project and returns filtered source files.
|
|
21
|
+
*/
|
|
22
|
+
export function loadProject(options) {
|
|
23
|
+
const tsConfigFilePath = path.resolve(options.projectPath, 'tsconfig.json');
|
|
24
|
+
if (!fs.existsSync(tsConfigFilePath)) {
|
|
25
|
+
throw new Error(`tsconfig.json not found at ${tsConfigFilePath}. Please ensure the project path is correct.`);
|
|
26
|
+
}
|
|
27
|
+
const project = new Project({ tsConfigFilePath });
|
|
28
|
+
let sourceFiles = project.getSourceFiles().filter((file) => {
|
|
29
|
+
const filePath = file.getFilePath();
|
|
30
|
+
// Exclude node_modules and declaration files
|
|
31
|
+
if (filePath.includes('node_modules'))
|
|
32
|
+
return false;
|
|
33
|
+
if (filePath.endsWith('.d.ts'))
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
// Apply include patterns (keep only files matching at least one pattern)
|
|
38
|
+
if (options.include && options.include.length > 0) {
|
|
39
|
+
const includePatterns = options.include;
|
|
40
|
+
sourceFiles = sourceFiles.filter((file) => {
|
|
41
|
+
const relativePath = path.relative(options.projectPath, file.getFilePath());
|
|
42
|
+
return includePatterns.some((pattern) => matchGlob(relativePath, pattern));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Apply exclude patterns (remove files matching any pattern)
|
|
46
|
+
if (options.exclude && options.exclude.length > 0) {
|
|
47
|
+
const excludePatterns = options.exclude;
|
|
48
|
+
sourceFiles = sourceFiles.filter((file) => {
|
|
49
|
+
const relativePath = path.relative(options.projectPath, file.getFilePath());
|
|
50
|
+
return !excludePatterns.some((pattern) => matchGlob(relativePath, pattern));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Check for syntax errors and skip problematic files
|
|
54
|
+
sourceFiles = sourceFiles.filter((file) => {
|
|
55
|
+
const diagnostics = file.getPreEmitDiagnostics();
|
|
56
|
+
const syntaxErrors = diagnostics.filter((d) => d.getCategory() === 1 // DiagnosticCategory.Error
|
|
57
|
+
);
|
|
58
|
+
if (syntaxErrors.length > 0) {
|
|
59
|
+
console.warn(`Warning: Skipping ${path.relative(options.projectPath, file.getFilePath())} due to ${syntaxErrors.length} syntax error(s)`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
if (sourceFiles.length === 0) {
|
|
65
|
+
console.warn('Warning: No source files found after filtering.');
|
|
66
|
+
}
|
|
67
|
+
return { project, sourceFiles };
|
|
68
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export function buildFileGraph(sourceFiles, projectPath) {
|
|
3
|
+
// Build a Set of all source file paths (absolute) for quick lookup
|
|
4
|
+
const sourceFilePaths = new Set(sourceFiles.map((f) => f.getFilePath()));
|
|
5
|
+
// Create FileNode for each source file
|
|
6
|
+
const files = sourceFiles.map((f) => ({
|
|
7
|
+
id: path.relative(projectPath, f.getFilePath()),
|
|
8
|
+
symbols: [],
|
|
9
|
+
}));
|
|
10
|
+
// Build edges from import declarations
|
|
11
|
+
const edgeSet = new Set();
|
|
12
|
+
const edges = [];
|
|
13
|
+
for (const sourceFile of sourceFiles) {
|
|
14
|
+
const sourceId = path.relative(projectPath, sourceFile.getFilePath());
|
|
15
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
16
|
+
const targetFile = importDecl.getModuleSpecifierSourceFile();
|
|
17
|
+
// Skip if unresolved or not in our source files set
|
|
18
|
+
if (!targetFile)
|
|
19
|
+
continue;
|
|
20
|
+
const targetPath = targetFile.getFilePath();
|
|
21
|
+
if (!sourceFilePaths.has(targetPath))
|
|
22
|
+
continue;
|
|
23
|
+
const targetId = path.relative(projectPath, targetPath);
|
|
24
|
+
const edgeKey = `${sourceId}::${targetId}`;
|
|
25
|
+
if (!edgeSet.has(edgeKey)) {
|
|
26
|
+
edgeSet.add(edgeKey);
|
|
27
|
+
edges.push({ source: sourceId, target: targetId });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { files, edges };
|
|
32
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { loadProject } from './analyzer.js';
|
|
4
|
+
import { buildFileGraph } from './file-graph.js';
|
|
5
|
+
import { buildSymbolGraph } from './symbol-graph.js';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program
|
|
10
|
+
.name('coderaph')
|
|
11
|
+
.description('TypeScript dependency graph 3D visualizer')
|
|
12
|
+
.version('0.1.0');
|
|
13
|
+
program
|
|
14
|
+
.command('analyze <project-path>')
|
|
15
|
+
.description('Analyze TypeScript project and generate dependency graph JSON')
|
|
16
|
+
.option('-o, --output <path>', 'Output JSON file path', 'coderaph-output.json')
|
|
17
|
+
.option('--include <patterns...>', 'Include glob patterns')
|
|
18
|
+
.option('--exclude <patterns...>', 'Exclude glob patterns')
|
|
19
|
+
.action((projectPath, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const resolvedPath = path.resolve(projectPath);
|
|
22
|
+
console.log(`Analyzing ${resolvedPath}...`);
|
|
23
|
+
const { sourceFiles } = loadProject({
|
|
24
|
+
projectPath: resolvedPath,
|
|
25
|
+
include: options.include,
|
|
26
|
+
exclude: options.exclude,
|
|
27
|
+
});
|
|
28
|
+
console.log(`Found ${sourceFiles.length} source files`);
|
|
29
|
+
if (sourceFiles.length === 0) {
|
|
30
|
+
console.warn('No source files to analyze');
|
|
31
|
+
}
|
|
32
|
+
const { files, edges: fileEdges } = buildFileGraph(sourceFiles, resolvedPath);
|
|
33
|
+
const { symbols, edges: symbolEdges } = buildSymbolGraph(sourceFiles, resolvedPath);
|
|
34
|
+
// Populate FileNode.symbols arrays
|
|
35
|
+
const fileSymbolMap = new Map();
|
|
36
|
+
for (const symbol of symbols) {
|
|
37
|
+
const existing = fileSymbolMap.get(symbol.fileId) || [];
|
|
38
|
+
existing.push(symbol.id);
|
|
39
|
+
fileSymbolMap.set(symbol.fileId, existing);
|
|
40
|
+
}
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
file.symbols = fileSymbolMap.get(file.id) || [];
|
|
43
|
+
}
|
|
44
|
+
const output = {
|
|
45
|
+
version: 1,
|
|
46
|
+
files,
|
|
47
|
+
symbols,
|
|
48
|
+
fileEdges,
|
|
49
|
+
symbolEdges,
|
|
50
|
+
};
|
|
51
|
+
const outputPath = path.resolve(options.output);
|
|
52
|
+
fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
|
|
53
|
+
console.log(`Output written to ${outputPath}`);
|
|
54
|
+
console.log(`Files: ${files.length}, Symbols: ${symbols.length}, File edges: ${fileEdges.length}, Symbol edges: ${symbolEdges.length}`);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
program
|
|
62
|
+
.command('view <json-path>')
|
|
63
|
+
.description('Start web viewer for dependency graph')
|
|
64
|
+
.option('-p, --port <number>', 'Server port', '3000')
|
|
65
|
+
.action(async (jsonPath, options) => {
|
|
66
|
+
const { startServer } = await import('./server.js');
|
|
67
|
+
startServer(path.resolve(jsonPath), parseInt(options.port, 10));
|
|
68
|
+
});
|
|
69
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function startServer(jsonPath: string, port: number): void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
const MIME_TYPES = {
|
|
7
|
+
'.html': 'text/html',
|
|
8
|
+
'.js': 'application/javascript',
|
|
9
|
+
'.css': 'text/css',
|
|
10
|
+
};
|
|
11
|
+
export function startServer(jsonPath, port) {
|
|
12
|
+
const absoluteJsonPath = path.resolve(jsonPath);
|
|
13
|
+
if (!fs.existsSync(absoluteJsonPath)) {
|
|
14
|
+
console.error(`Error: file not found — ${absoluteJsonPath}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const webDir = fileURLToPath(new URL('../../src/web/', import.meta.url));
|
|
18
|
+
const server = http.createServer((req, res) => {
|
|
19
|
+
const url = req.url ?? '/';
|
|
20
|
+
if (req.method === 'GET' && url === '/api/graph') {
|
|
21
|
+
fs.readFile(absoluteJsonPath, 'utf-8', (err, data) => {
|
|
22
|
+
if (err) {
|
|
23
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(JSON.stringify({ error: 'Failed to read graph file' }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
28
|
+
res.end(data);
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Serve static files from web directory
|
|
33
|
+
const filePath = path.join(webDir, url === '/' ? 'index.html' : url);
|
|
34
|
+
// Prevent directory traversal
|
|
35
|
+
if (!filePath.startsWith(webDir)) {
|
|
36
|
+
res.writeHead(403);
|
|
37
|
+
res.end('Forbidden');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
fs.readFile(filePath, (err, data) => {
|
|
41
|
+
if (err) {
|
|
42
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
43
|
+
res.end('Not Found');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const ext = path.extname(filePath);
|
|
47
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
48
|
+
res.writeHead(200, { 'Content-Type': contentType });
|
|
49
|
+
res.end(data);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
server.listen(port, () => {
|
|
53
|
+
const url = `http://localhost:${port}`;
|
|
54
|
+
console.log(`Coderaph viewer running at ${url}`);
|
|
55
|
+
// Open browser automatically
|
|
56
|
+
switch (process.platform) {
|
|
57
|
+
case 'darwin':
|
|
58
|
+
execFile('open', [url], () => { });
|
|
59
|
+
break;
|
|
60
|
+
case 'win32':
|
|
61
|
+
execFile('cmd', ['/c', 'start', url], () => { });
|
|
62
|
+
break;
|
|
63
|
+
default:
|
|
64
|
+
execFile('xdg-open', [url], () => { });
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { SyntaxKind } from 'ts-morph';
|
|
3
|
+
export function buildSymbolGraph(sourceFiles, projectPath) {
|
|
4
|
+
const allSymbols = [];
|
|
5
|
+
// Map: filePath -> Map<symbolName, symbolId>
|
|
6
|
+
const fileSymbolMap = new Map();
|
|
7
|
+
// 1. Extract top-level symbols from each file
|
|
8
|
+
for (const file of sourceFiles) {
|
|
9
|
+
const filePath = file.getFilePath();
|
|
10
|
+
const fileId = path.relative(projectPath, filePath);
|
|
11
|
+
const nameToId = new Map();
|
|
12
|
+
const addSymbol = (name, kind, node) => {
|
|
13
|
+
if (!name)
|
|
14
|
+
return;
|
|
15
|
+
const id = `${fileId}#${name}`;
|
|
16
|
+
allSymbols.push({ id, name, kind, fileId, node });
|
|
17
|
+
nameToId.set(name, id);
|
|
18
|
+
};
|
|
19
|
+
for (const cls of file.getClasses()) {
|
|
20
|
+
addSymbol(cls.getName(), 'class', cls);
|
|
21
|
+
}
|
|
22
|
+
for (const fn of file.getFunctions()) {
|
|
23
|
+
addSymbol(fn.getName(), 'function', fn);
|
|
24
|
+
}
|
|
25
|
+
for (const iface of file.getInterfaces()) {
|
|
26
|
+
addSymbol(iface.getName(), 'interface', iface);
|
|
27
|
+
}
|
|
28
|
+
for (const alias of file.getTypeAliases()) {
|
|
29
|
+
addSymbol(alias.getName(), 'type', alias);
|
|
30
|
+
}
|
|
31
|
+
for (const en of file.getEnums()) {
|
|
32
|
+
addSymbol(en.getName(), 'enum', en);
|
|
33
|
+
}
|
|
34
|
+
for (const stmt of file.getVariableStatements()) {
|
|
35
|
+
for (const decl of stmt.getDeclarations()) {
|
|
36
|
+
addSymbol(decl.getName(), 'variable', decl);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
fileSymbolMap.set(filePath, nameToId);
|
|
40
|
+
}
|
|
41
|
+
// 2. Build edges by resolving imports and tracking references
|
|
42
|
+
const edgeSet = new Set();
|
|
43
|
+
const edges = [];
|
|
44
|
+
for (const file of sourceFiles) {
|
|
45
|
+
// Build import map: importedName -> target symbol ID
|
|
46
|
+
const importMap = new Map();
|
|
47
|
+
for (const importDecl of file.getImportDeclarations()) {
|
|
48
|
+
const targetFile = importDecl.getModuleSpecifierSourceFile();
|
|
49
|
+
if (!targetFile)
|
|
50
|
+
continue;
|
|
51
|
+
const targetSymbols = fileSymbolMap.get(targetFile.getFilePath());
|
|
52
|
+
if (!targetSymbols)
|
|
53
|
+
continue;
|
|
54
|
+
// Named imports
|
|
55
|
+
for (const named of importDecl.getNamedImports()) {
|
|
56
|
+
const importedName = named.getAliasNode()?.getText() ?? named.getName();
|
|
57
|
+
const originalName = named.getName();
|
|
58
|
+
const targetId = targetSymbols.get(originalName);
|
|
59
|
+
if (targetId) {
|
|
60
|
+
importMap.set(importedName, targetId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Default import
|
|
64
|
+
const defaultImport = importDecl.getDefaultImport();
|
|
65
|
+
if (defaultImport) {
|
|
66
|
+
const localName = defaultImport.getText();
|
|
67
|
+
// Default exports typically match a symbol named "default" or the class/function name
|
|
68
|
+
// Try to find a matching symbol — check 'default' first, then the local name
|
|
69
|
+
const targetId = targetSymbols.get('default') ?? targetSymbols.get(localName);
|
|
70
|
+
if (targetId) {
|
|
71
|
+
importMap.set(localName, targetId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (importMap.size === 0)
|
|
76
|
+
continue;
|
|
77
|
+
// For each symbol in this file, find references to imported names
|
|
78
|
+
const fileId = path.relative(projectPath, file.getFilePath());
|
|
79
|
+
const fileEntries = allSymbols.filter((s) => s.fileId === fileId);
|
|
80
|
+
for (const symbol of fileEntries) {
|
|
81
|
+
try {
|
|
82
|
+
const identifiers = symbol.node.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
for (const ident of identifiers) {
|
|
85
|
+
const text = ident.getText();
|
|
86
|
+
if (seen.has(text))
|
|
87
|
+
continue;
|
|
88
|
+
const targetId = importMap.get(text);
|
|
89
|
+
if (targetId) {
|
|
90
|
+
seen.add(text);
|
|
91
|
+
const edgeKey = `${symbol.id}::${targetId}`;
|
|
92
|
+
if (!edgeSet.has(edgeKey)) {
|
|
93
|
+
edgeSet.add(edgeKey);
|
|
94
|
+
edges.push({ source: symbol.id, target: targetId });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
console.warn(`[symbol-graph] Warning: failed to resolve references for symbol "${symbol.id}":`, err instanceof Error ? err.message : err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Convert to SymbolNode[] (strip the `node` property)
|
|
105
|
+
const symbols = allSymbols.map(({ id, name, kind, fileId }) => ({
|
|
106
|
+
id,
|
|
107
|
+
name,
|
|
108
|
+
kind,
|
|
109
|
+
fileId,
|
|
110
|
+
}));
|
|
111
|
+
return { symbols, edges };
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type SymbolKind = 'class' | 'function' | 'interface' | 'type' | 'enum' | 'variable';
|
|
2
|
+
export interface FileNode {
|
|
3
|
+
id: string;
|
|
4
|
+
symbols: string[];
|
|
5
|
+
}
|
|
6
|
+
export interface SymbolNode {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
kind: SymbolKind;
|
|
10
|
+
fileId: string;
|
|
11
|
+
}
|
|
12
|
+
export interface Edge {
|
|
13
|
+
source: string;
|
|
14
|
+
target: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CoderaphOutput {
|
|
17
|
+
version: 1;
|
|
18
|
+
files: FileNode[];
|
|
19
|
+
symbols: SymbolNode[];
|
|
20
|
+
fileEdges: Edge[];
|
|
21
|
+
symbolEdges: Edge[];
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "coderaph",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript dependency graph 3D visualizer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"coderaph": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"src/web/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.0.0",
|
|
19
|
+
"ts-morph": "^25.0.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typescript": "^5.7.0"
|
|
24
|
+
}
|
|
25
|
+
}
|