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 ADDED
@@ -0,0 +1,126 @@
1
+ # Coderaph
2
+
3
+ TypeScript 코드베이스의 의존성 관계를 인터랙티브 3D 그래프로 시각화하는 CLI 도구.
4
+
5
+ ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)
6
+ ![Three.js](https://img.shields.io/badge/Three.js-000000?logo=three.js&logoColor=white)
7
+ ![License](https://img.shields.io/badge/license-MIT-green)
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,6 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import { FileNode, Edge } from '../types/graph.js';
3
+ export declare function buildFileGraph(sourceFiles: SourceFile[], projectPath: string): {
4
+ files: FileNode[];
5
+ edges: Edge[];
6
+ };
@@ -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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,6 @@
1
+ import { SourceFile } from 'ts-morph';
2
+ import { SymbolNode, Edge } from '../types/graph.js';
3
+ export declare function buildSymbolGraph(sourceFiles: SourceFile[], projectPath: string): {
4
+ symbols: SymbolNode[];
5
+ edges: Edge[];
6
+ };
@@ -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
+ }