coderaph 0.1.0 → 0.3.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.
@@ -1,9 +1,16 @@
1
1
  import { Project, SourceFile } from 'ts-morph';
2
2
  export interface AnalyzerOptions {
3
3
  projectPath: string;
4
+ tsConfigPath?: string;
4
5
  include?: string[];
5
6
  exclude?: string[];
7
+ prefix?: string;
6
8
  }
9
+ /**
10
+ * Reads the `name` field from package.json in the given directory.
11
+ * Returns undefined if the file doesn't exist or can't be parsed.
12
+ */
13
+ export declare function readPackageName(projectPath: string): string | undefined;
7
14
  /**
8
15
  * Loads a TypeScript project and returns filtered source files.
9
16
  */
@@ -1,6 +1,21 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { Project } from 'ts-morph';
4
+ /**
5
+ * Reads the `name` field from package.json in the given directory.
6
+ * Returns undefined if the file doesn't exist or can't be parsed.
7
+ */
8
+ export function readPackageName(projectPath) {
9
+ try {
10
+ const pkgPath = path.resolve(projectPath, 'package.json');
11
+ const content = fs.readFileSync(pkgPath, 'utf-8');
12
+ const pkg = JSON.parse(content);
13
+ return typeof pkg.name === 'string' ? pkg.name : undefined;
14
+ }
15
+ catch {
16
+ return undefined;
17
+ }
18
+ }
4
19
  /**
5
20
  * Simple glob matcher that supports `*` as a wildcard for any characters.
6
21
  * If the pattern contains no `*`, it falls back to substring matching.
@@ -16,13 +31,55 @@ function matchGlob(filePath, pattern) {
16
31
  const regex = new RegExp(`^${escaped}$`);
17
32
  return regex.test(filePath);
18
33
  }
34
+ const TSCONFIG_CANDIDATES = [
35
+ 'tsconfig.json',
36
+ 'tsconfig.build.json',
37
+ 'tsconfig.base.json',
38
+ 'tsconfig.app.json',
39
+ ];
40
+ /**
41
+ * Finds a tsconfig file in the given directory.
42
+ * Checks common tsconfig filenames, then searches subdirectories (1 level deep).
43
+ */
44
+ function findTsConfig(projectPath) {
45
+ // Check common names in project root
46
+ for (const candidate of TSCONFIG_CANDIDATES) {
47
+ const fullPath = path.resolve(projectPath, candidate);
48
+ if (fs.existsSync(fullPath))
49
+ return fullPath;
50
+ }
51
+ // Search one level of subdirectories (for monorepos: packages/*, apps/*)
52
+ const entries = fs.readdirSync(projectPath, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ if (!entry.isDirectory() || entry.name === 'node_modules' || entry.name.startsWith('.'))
55
+ continue;
56
+ const subTsConfig = path.resolve(projectPath, entry.name, 'tsconfig.json');
57
+ if (fs.existsSync(subTsConfig))
58
+ return subTsConfig;
59
+ }
60
+ return null;
61
+ }
19
62
  /**
20
63
  * Loads a TypeScript project and returns filtered source files.
21
64
  */
22
65
  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.`);
66
+ let tsConfigFilePath;
67
+ if (options.tsConfigPath) {
68
+ tsConfigFilePath = path.resolve(options.projectPath, options.tsConfigPath);
69
+ if (!fs.existsSync(tsConfigFilePath)) {
70
+ throw new Error(`tsconfig not found at ${tsConfigFilePath}. Please check the --tsconfig path.`);
71
+ }
72
+ }
73
+ else {
74
+ const found = findTsConfig(options.projectPath);
75
+ if (!found) {
76
+ throw new Error(`No tsconfig.json found in ${options.projectPath} or its subdirectories. ` +
77
+ `Use --tsconfig to specify the path (e.g. --tsconfig packages/app/tsconfig.json).`);
78
+ }
79
+ tsConfigFilePath = found;
80
+ if (path.basename(tsConfigFilePath) !== 'tsconfig.json' || path.dirname(tsConfigFilePath) !== options.projectPath) {
81
+ console.log(`Using tsconfig: ${path.relative(options.projectPath, tsConfigFilePath)}`);
82
+ }
26
83
  }
27
84
  const project = new Project({ tsConfigFilePath });
28
85
  let sourceFiles = project.getSourceFiles().filter((file) => {
@@ -50,13 +107,12 @@ export function loadProject(options) {
50
107
  return !excludePatterns.some((pattern) => matchGlob(relativePath, pattern));
51
108
  });
52
109
  }
53
- // Check for syntax errors and skip problematic files
110
+ // Skip files with actual syntax errors (not type errors — those are fine for graph analysis)
54
111
  sourceFiles = sourceFiles.filter((file) => {
55
- const diagnostics = file.getPreEmitDiagnostics();
56
- const syntaxErrors = diagnostics.filter((d) => d.getCategory() === 1 // DiagnosticCategory.Error
112
+ const syntaxDiagnostics = file.getPreEmitDiagnostics().filter((d) => d.getCode() >= 1000 && d.getCode() < 2000 // TS1xxx = syntax errors only
57
113
  );
58
- if (syntaxErrors.length > 0) {
59
- console.warn(`Warning: Skipping ${path.relative(options.projectPath, file.getFilePath())} due to ${syntaxErrors.length} syntax error(s)`);
114
+ if (syntaxDiagnostics.length > 0) {
115
+ console.warn(`Warning: Skipping ${path.relative(options.projectPath, file.getFilePath())} due to ${syntaxDiagnostics.length} syntax error(s)`);
60
116
  return false;
61
117
  }
62
118
  return true;
package/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
- import { loadProject } from './analyzer.js';
3
+ import { loadProject, readPackageName } from './analyzer.js';
4
4
  import { buildFileGraph } from './file-graph.js';
5
5
  import { buildSymbolGraph } from './symbol-graph.js';
6
6
  import fs from 'node:fs';
@@ -9,61 +9,130 @@ const program = new Command();
9
9
  program
10
10
  .name('coderaph')
11
11
  .description('TypeScript dependency graph 3D visualizer')
12
- .version('0.1.0');
12
+ .version('0.3.0');
13
13
  program
14
14
  .command('analyze <project-path>')
15
15
  .description('Analyze TypeScript project and generate dependency graph JSON')
16
16
  .option('-o, --output <path>', 'Output JSON file path', 'coderaph-output.json')
17
+ .option('--tsconfig <path>', 'Path to tsconfig.json (relative to project path)')
17
18
  .option('--include <patterns...>', 'Include glob patterns')
18
19
  .option('--exclude <patterns...>', 'Exclude glob patterns')
20
+ .option('--prefix <path>', 'Prefix to prepend to all file IDs (e.g., "packages/core")')
19
21
  .action((projectPath, options) => {
20
22
  try {
21
23
  const resolvedPath = path.resolve(projectPath);
22
24
  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
- };
25
+ const output = runAnalyze(resolvedPath, options);
51
26
  const outputPath = path.resolve(options.output);
52
27
  fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
53
28
  console.log(`Output written to ${outputPath}`);
54
- console.log(`Files: ${files.length}, Symbols: ${symbols.length}, File edges: ${fileEdges.length}, Symbol edges: ${symbolEdges.length}`);
29
+ console.log(`Files: ${output.files.length}, Symbols: ${output.symbols.length}, File edges: ${output.fileEdges.length}, Symbol edges: ${output.symbolEdges.length}`);
55
30
  }
56
31
  catch (err) {
57
32
  console.error(`Error: ${err instanceof Error ? err.message : err}`);
58
33
  process.exit(1);
59
34
  }
60
35
  });
36
+ /**
37
+ * Runs the analysis pipeline and returns a CoderaphOutput.
38
+ * Extracted so it can be used by both the `analyze` and `view --analyze` commands.
39
+ */
40
+ function runAnalyze(resolvedPath, options) {
41
+ const { sourceFiles } = loadProject({
42
+ projectPath: resolvedPath,
43
+ tsConfigPath: options.tsconfig,
44
+ include: options.include,
45
+ exclude: options.exclude,
46
+ prefix: options.prefix,
47
+ });
48
+ console.log(`Found ${sourceFiles.length} source files`);
49
+ if (sourceFiles.length === 0) {
50
+ console.warn('No source files to analyze');
51
+ }
52
+ const { files, edges: fileEdges } = buildFileGraph(sourceFiles, resolvedPath);
53
+ const { symbols, edges: symbolEdges } = buildSymbolGraph(sourceFiles, resolvedPath);
54
+ // Populate FileNode.symbols arrays
55
+ const fileSymbolMap = new Map();
56
+ for (const symbol of symbols) {
57
+ const existing = fileSymbolMap.get(symbol.fileId) || [];
58
+ existing.push(symbol.id);
59
+ fileSymbolMap.set(symbol.fileId, existing);
60
+ }
61
+ for (const file of files) {
62
+ file.symbols = fileSymbolMap.get(file.id) || [];
63
+ }
64
+ const packageName = readPackageName(resolvedPath);
65
+ const prefix = options.prefix;
66
+ // Apply prefix to all file IDs if provided
67
+ if (prefix) {
68
+ const addPrefix = (id) => `${prefix}/${id}`;
69
+ for (const file of files) {
70
+ file.id = addPrefix(file.id);
71
+ file.symbols = file.symbols.map(addPrefix);
72
+ }
73
+ for (const symbol of symbols) {
74
+ symbol.id = addPrefix(symbol.id);
75
+ symbol.fileId = addPrefix(symbol.fileId);
76
+ }
77
+ for (const edge of fileEdges) {
78
+ edge.source = addPrefix(edge.source);
79
+ edge.target = addPrefix(edge.target);
80
+ }
81
+ for (const edge of symbolEdges) {
82
+ edge.source = addPrefix(edge.source);
83
+ edge.target = addPrefix(edge.target);
84
+ }
85
+ }
86
+ const useV2 = !!(prefix || packageName);
87
+ const output = {
88
+ version: useV2 ? 2 : 1,
89
+ ...(packageName ? { name: packageName } : {}),
90
+ ...(prefix ? { prefix } : {}),
91
+ files,
92
+ symbols,
93
+ fileEdges,
94
+ symbolEdges,
95
+ };
96
+ return output;
97
+ }
61
98
  program
62
- .command('view <json-path>')
99
+ .command('view [json-path...]')
63
100
  .description('Start web viewer for dependency graph')
64
101
  .option('-p, --port <number>', 'Server port', '3000')
65
- .action(async (jsonPath, options) => {
102
+ .option('--analyze <project-path>', 'Run analysis in-memory and start viewer')
103
+ .option('--tsconfig <path>', 'Path to tsconfig.json (used with --analyze)')
104
+ .option('--include <patterns...>', 'Include glob patterns (used with --analyze)')
105
+ .option('--exclude <patterns...>', 'Exclude glob patterns (used with --analyze)')
106
+ .option('-o, --output <path>', 'Save analysis result to file (used with --analyze)')
107
+ .option('--prefix <path>', 'Prefix for file IDs (used with --analyze)')
108
+ .action(async (jsonPaths, options) => {
66
109
  const { startServer } = await import('./server.js');
67
- startServer(path.resolve(jsonPath), parseInt(options.port, 10));
110
+ const port = parseInt(options.port, 10);
111
+ if (options.analyze) {
112
+ // Run analysis in-memory
113
+ const resolvedPath = path.resolve(options.analyze);
114
+ console.log(`Analyzing ${resolvedPath}...`);
115
+ const output = runAnalyze(resolvedPath, {
116
+ tsconfig: options.tsconfig,
117
+ include: options.include,
118
+ exclude: options.exclude,
119
+ prefix: options.prefix,
120
+ });
121
+ // Optionally save to file
122
+ if (options.output) {
123
+ const outputPath = path.resolve(options.output);
124
+ fs.writeFileSync(outputPath, JSON.stringify(output, null, 2));
125
+ console.log(`Output written to ${outputPath}`);
126
+ }
127
+ startServer(output, port);
128
+ }
129
+ else if (jsonPaths && jsonPaths.length > 0) {
130
+ const resolvedPaths = jsonPaths.map((p) => path.resolve(p));
131
+ startServer(resolvedPaths, port);
132
+ }
133
+ else {
134
+ console.error('Error: provide either JSON file path(s) or --analyze <project-path>');
135
+ process.exit(1);
136
+ }
68
137
  });
69
138
  program.parse();
@@ -1 +1,2 @@
1
- export declare function startServer(jsonPath: string, port: number): void;
1
+ import type { CoderaphOutput } from '../types/graph.js';
2
+ export declare function startServer(source: string[] | CoderaphOutput, port: number): void;
@@ -8,25 +8,109 @@ const MIME_TYPES = {
8
8
  '.js': 'application/javascript',
9
9
  '.css': 'text/css',
10
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);
11
+ /**
12
+ * Reads multiple JSON files and merges them into a single CoderaphOutput.
13
+ * Cross-package edges are inferred by suffix matching of file IDs across packages.
14
+ */
15
+ function mergeJsonFiles(jsonPaths) {
16
+ const outputs = jsonPaths.map((p) => {
17
+ const absolutePath = path.resolve(p);
18
+ if (!fs.existsSync(absolutePath)) {
19
+ console.error(`Error: file not found — ${absolutePath}`);
20
+ process.exit(1);
21
+ }
22
+ return JSON.parse(fs.readFileSync(absolutePath, 'utf-8'));
23
+ });
24
+ if (outputs.length === 1) {
25
+ return outputs[0];
26
+ }
27
+ const merged = {
28
+ version: 2,
29
+ files: [],
30
+ symbols: [],
31
+ fileEdges: [],
32
+ symbolEdges: [],
33
+ };
34
+ // Build a map of file ID suffix -> full file ID for cross-package edge inference
35
+ // Key: filename suffix (e.g., "src/utils/helper.ts"), Value: array of full IDs from different packages
36
+ const fileIdsByPackage = new Map();
37
+ const allFileIds = new Set();
38
+ for (let i = 0; i < outputs.length; i++) {
39
+ const output = outputs[i];
40
+ merged.files.push(...output.files);
41
+ merged.symbols.push(...output.symbols);
42
+ merged.fileEdges.push(...output.fileEdges);
43
+ merged.symbolEdges.push(...output.symbolEdges);
44
+ const ids = new Set(output.files.map((f) => f.id));
45
+ fileIdsByPackage.set(i, ids);
46
+ for (const id of ids) {
47
+ allFileIds.add(id);
48
+ }
49
+ }
50
+ // Cross-package edge inference:
51
+ // For each edge target in each package, if the target doesn't exist in the same package's files
52
+ // but matches a file ID in another package by suffix, add a cross-package edge.
53
+ for (let i = 0; i < outputs.length; i++) {
54
+ const pkgFileIds = fileIdsByPackage.get(i);
55
+ const existingEdgeTargets = new Set(outputs[i].fileEdges.map((e) => `${e.source}->${e.target}`));
56
+ for (const edge of outputs[i].fileEdges) {
57
+ // If the target is not in any known file, try suffix matching
58
+ if (!allFileIds.has(edge.target)) {
59
+ for (const [j, otherIds] of fileIdsByPackage) {
60
+ if (i === j)
61
+ continue;
62
+ for (const otherId of otherIds) {
63
+ if (otherId.endsWith(edge.target) || edge.target.endsWith(otherId)) {
64
+ const edgeKey = `${edge.source}->${otherId}`;
65
+ if (!existingEdgeTargets.has(edgeKey)) {
66
+ merged.fileEdges.push({ source: edge.source, target: otherId });
67
+ existingEdgeTargets.add(edgeKey);
68
+ }
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
16
74
  }
75
+ return merged;
76
+ }
77
+ export function startServer(source, port) {
17
78
  const webDir = fileURLToPath(new URL('../../src/web/', import.meta.url));
79
+ // Resolve the graph data
80
+ let graphData = null;
81
+ let jsonPaths = null;
82
+ if (Array.isArray(source) && source.length > 0 && typeof source[0] === 'string') {
83
+ jsonPaths = source;
84
+ // Validate files exist upfront
85
+ for (const p of jsonPaths) {
86
+ const absolutePath = path.resolve(p);
87
+ if (!fs.existsSync(absolutePath)) {
88
+ console.error(`Error: file not found — ${absolutePath}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+ }
93
+ else {
94
+ graphData = source;
95
+ }
18
96
  const server = http.createServer((req, res) => {
19
97
  const url = req.url ?? '/';
20
98
  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;
99
+ try {
100
+ let data;
101
+ if (graphData) {
102
+ data = graphData;
103
+ }
104
+ else {
105
+ data = mergeJsonFiles(jsonPaths);
26
106
  }
27
107
  res.writeHead(200, { 'Content-Type': 'application/json' });
28
- res.end(data);
29
- });
108
+ res.end(JSON.stringify(data));
109
+ }
110
+ catch (err) {
111
+ res.writeHead(500, { 'Content-Type': 'application/json' });
112
+ res.end(JSON.stringify({ error: 'Failed to read graph file' }));
113
+ }
30
114
  return;
31
115
  }
32
116
  // Serve static files from web directory
@@ -14,7 +14,9 @@ export interface Edge {
14
14
  target: string;
15
15
  }
16
16
  export interface CoderaphOutput {
17
- version: 1;
17
+ version: 1 | 2;
18
+ name?: string;
19
+ prefix?: string;
18
20
  files: FileNode[];
19
21
  symbols: SymbolNode[];
20
22
  fileEdges: Edge[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderaph",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript dependency graph 3D visualizer",
5
5
  "type": "module",
6
6
  "bin": {
package/src/web/app.js CHANGED
@@ -579,9 +579,35 @@ function matchGroupQuery(query, node) {
579
579
  return name.includes(q) || id.includes(q) || file.includes(q);
580
580
  }
581
581
 
582
+ function nodeMatchesSubstring(node, val) {
583
+ const name = (node.name || '').toLowerCase();
584
+ const id = node.id.toLowerCase();
585
+ const file = (node.fileId || '').toLowerCase();
586
+ return name.includes(val) || id.includes(val) || file.includes(val);
587
+ }
588
+
582
589
  function computeGroupNodeIds(query) {
583
590
  if (!graphData || !query.trim()) return [];
591
+ const q = query.trim().toLowerCase();
584
592
  const nodes = currentMode === 'file' ? graphData.files : graphData.symbols;
593
+ const edges = currentMode === 'file' ? graphData.fileEdges : graphData.symbolEdges;
594
+
595
+ // deps:X — nodes that import/depend on X (edges where target matches X)
596
+ if (q.startsWith('deps:')) {
597
+ const val = q.slice(5);
598
+ if (!val) return [];
599
+ const targetIds = new Set(nodes.filter(n => nodeMatchesSubstring(n, val)).map(n => n.id));
600
+ return edges.filter(e => targetIds.has(e.target)).map(e => e.source).filter((v, i, a) => a.indexOf(v) === i);
601
+ }
602
+
603
+ // rdeps:X — nodes that X depends on (edges where source matches X)
604
+ if (q.startsWith('rdeps:')) {
605
+ const val = q.slice(6);
606
+ if (!val) return [];
607
+ const sourceIds = new Set(nodes.filter(n => nodeMatchesSubstring(n, val)).map(n => n.id));
608
+ return edges.filter(e => sourceIds.has(e.source)).map(e => e.target).filter((v, i, a) => a.indexOf(v) === i);
609
+ }
610
+
585
611
  const ids = [];
586
612
  for (const node of nodes) {
587
613
  if (matchGroupQuery(query, node)) ids.push(node.id);
@@ -568,7 +568,7 @@ function createGroupRow(query, colorNum) {
568
568
  const input = document.createElement('input');
569
569
  input.type = 'text';
570
570
  input.className = 'group-query';
571
- input.placeholder = '검색어 입력... (path: file: kind:)';
571
+ input.placeholder = '검색어 입력... (path: file: kind: deps: rdeps:)';
572
572
  input.value = query || '';
573
573
 
574
574
  const colorInput = document.createElement('input');
@@ -615,6 +615,10 @@ function createGroupRow(query, colorNum) {
615
615
  el.value = 'file:' + (item.fileId || item.id).split('/').pop();
616
616
  } else if (val.startsWith('kind:')) {
617
617
  el.value = 'kind:' + (item.kind || 'file');
618
+ } else if (val.startsWith('deps:')) {
619
+ el.value = 'deps:' + (item.name || item.id.split('/').pop());
620
+ } else if (val.startsWith('rdeps:')) {
621
+ el.value = 'rdeps:' + (item.name || item.id.split('/').pop());
618
622
  } else {
619
623
  el.value = item.name || item.id.split('/').pop();
620
624
  }
@@ -656,6 +660,27 @@ function getGroupQueryCandidates(rawQuery) {
656
660
  for (const item of items) kinds.add(item.kind || 'file');
657
661
  return [...kinds].filter(k => k.includes(val)).map(k => ({ id: k, name: k, kind: k }));
658
662
  }
663
+ // For deps:/rdeps: prefix, return matching nodes as candidates
664
+ if (q.startsWith('deps:')) {
665
+ const val = q.slice(5);
666
+ if (!val) return items;
667
+ return items.filter(n => {
668
+ const name = (n.name || '').toLowerCase();
669
+ const id = n.id.toLowerCase();
670
+ const file = (n.fileId || '').toLowerCase();
671
+ return name.includes(val) || id.includes(val) || file.includes(val);
672
+ });
673
+ }
674
+ if (q.startsWith('rdeps:')) {
675
+ const val = q.slice(6);
676
+ if (!val) return items;
677
+ return items.filter(n => {
678
+ const name = (n.name || '').toLowerCase();
679
+ const id = n.id.toLowerCase();
680
+ const file = (n.fileId || '').toLowerCase();
681
+ return name.includes(val) || id.includes(val) || file.includes(val);
682
+ });
683
+ }
659
684
  return items;
660
685
  }
661
686
 
@@ -669,6 +694,8 @@ function showGroupHint(inputEl) {
669
694
  { prefix: 'path:', desc: '일치하는 파일 경로' },
670
695
  { prefix: 'file:', desc: '일치하는 파일 이름' },
671
696
  { prefix: 'kind:', desc: 'class, function, interface 등' },
697
+ { prefix: 'deps:', desc: '해당 노드를 import하는 노드' },
698
+ { prefix: 'rdeps:', desc: '해당 노드가 의존하는 노드' },
672
699
  ];
673
700
  for (const opt of options) {
674
701
  const item = document.createElement('div');