Skip to content

Instantly share code, notes, and snippets.

@grrowl
Last active July 2, 2025 06:52
Show Gist options
  • Save grrowl/d31e80250f900c8122185848e0bad06e to your computer and use it in GitHub Desktop.
Save grrowl/d31e80250f900c8122185848e0bad06e to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
import * as fs from "fs";
import * as path from "path";
import * as ts from "typescript";
interface ModuleInfo {
filePath: string;
exports: Map<string, string>; // exported name -> type (function, class, interface, etc.)
imports: Map<string, string>; // imported name -> source module
functionCalls: Set<string>;
}
interface DependencyGraph {
modules: Map<string, ModuleInfo>;
connections: Array<{
from: string;
to: string;
exportName: string;
importName: string;
exportType?: string;
}>;
domains: Map<string, Set<string>>; // domain -> module paths
}
class TypeScriptAnalyzer {
private program: ts.Program;
private typeChecker: ts.TypeChecker;
private graph: DependencyGraph = {
modules: new Map(),
connections: [],
domains: new Map(),
};
constructor(
private projectPath: string,
private tsconfigPath?: string,
) {
// Resolve to absolute path to ensure consistent comparisons
this.projectPath = path.resolve(projectPath);
const configPath = this.tsconfigPath || this.findTsConfig();
const configFile = ts.readConfigFile(configPath, ts.sys.readFile);
if (configFile.error) {
throw new Error(
`Error reading tsconfig: ${configFile.error.messageText}`,
);
}
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(configPath),
);
if (parsedConfig.errors.length > 0) {
console.error(
"TSConfig errors:",
parsedConfig.errors.map((e) => e.messageText),
);
}
this.program = ts.createProgram(
parsedConfig.fileNames,
parsedConfig.options,
);
this.typeChecker = this.program.getTypeChecker();
}
private findTsConfig(): string {
let dir = this.projectPath;
// If projectPath is a file, start from its directory
if (fs.existsSync(dir) && fs.statSync(dir).isFile()) {
dir = path.dirname(dir);
}
while (dir !== path.dirname(dir)) {
const configPath = path.join(dir, "tsconfig.json");
if (fs.existsSync(configPath)) {
return configPath;
}
dir = path.dirname(dir);
}
// Fallback: check current working directory
const cwdConfig = path.join(process.cwd(), "tsconfig.json");
if (fs.existsSync(cwdConfig)) {
return cwdConfig;
}
throw new Error("tsconfig.json not found");
}
private getRelativeModulePath(filePath: string): string {
const relativePath = path.relative(this.projectPath, filePath);
// Normalize path separators for cross-platform compatibility
return relativePath.replace(/\\/g, "/");
}
private resolveImportPath(importPath: string, currentFile: string): string {
if (importPath.startsWith(".")) {
// Relative import
const currentDir = path.dirname(currentFile);
const resolved = path.resolve(currentDir, importPath);
const extensions = [".ts", ".tsx", ".js", ".jsx"];
// Try exact path first
for (const ext of extensions) {
const withExt = resolved + ext;
if (fs.existsSync(withExt)) {
return this.getRelativeModulePath(withExt);
}
}
// Try index file
for (const ext of extensions) {
const indexPath = path.join(resolved, "index" + ext);
if (fs.existsSync(indexPath)) {
return this.getRelativeModulePath(indexPath);
}
}
// Try without extension (already exists)
if (fs.existsSync(resolved)) {
return this.getRelativeModulePath(resolved);
}
// Return as-is if we can't resolve (might be in our module list anyway)
return this.getRelativeModulePath(resolved);
}
// External module or absolute path - return as-is
return importPath;
}
analyze(verbose: boolean = false): DependencyGraph {
const allSourceFiles = this.program.getSourceFiles();
if (verbose) {
console.error(`Found ${allSourceFiles.length} total source files`);
console.error(`Project path: ${this.projectPath}`);
console.error(`Absolute project path: ${path.resolve(this.projectPath)}`);
}
const sourceFiles = allSourceFiles.filter((sf) => {
const isNotDeclaration = !sf.isDeclarationFile;
const isInProject =
sf.fileName.includes(path.resolve(this.projectPath)) ||
sf.fileName.includes(this.projectPath);
const isNotNodeModules = !sf.fileName.includes("node_modules");
if (verbose) {
console.error(`File: ${sf.fileName}`);
console.error(` - Not declaration: ${isNotDeclaration}`);
console.error(` - In project: ${isInProject}`);
console.error(` - Not node_modules: ${isNotNodeModules}`);
console.error(
` - Included: ${isNotDeclaration && isInProject && isNotNodeModules}`,
);
}
return isNotDeclaration && isInProject && isNotNodeModules;
});
if (verbose) {
console.error(`Filtered to ${sourceFiles.length} project source files`);
}
// First pass: collect all exports
sourceFiles.forEach((sourceFile) => {
const modulePath = this.getRelativeModulePath(sourceFile.fileName);
const moduleInfo: ModuleInfo = {
filePath: modulePath,
exports: new Map(),
imports: new Map(),
functionCalls: new Set(),
};
this.extractModuleInfo(sourceFile, moduleInfo);
this.graph.modules.set(modulePath, moduleInfo);
if (verbose) {
console.error(`Processed module: ${modulePath}`);
console.error(
` - Exports: ${Array.from(moduleInfo.exports.entries())
.map(([name, type]) => `${type} ${name}`)
.join(", ")}`,
);
console.error(
` - Imports: ${Array.from(moduleInfo.imports.keys()).join(", ")}`,
);
}
});
// Second pass: resolve import-export connections
this.graph.modules.forEach((moduleInfo, modulePath) => {
moduleInfo.imports.forEach((sourcePath, importName) => {
const resolvedPath = this.resolveImportPath(
sourcePath,
path.join(this.projectPath, modulePath),
);
const sourceModule = this.graph.modules.get(resolvedPath);
if (sourceModule && resolvedPath !== modulePath) {
// Avoid self-references
// Check if the import matches an export
const hasExport =
sourceModule.exports.has(importName) ||
sourceModule.exports.has("default") ||
sourceModule.exports.has("*") ||
(importName === "*" && sourceModule.exports.size > 0);
if (hasExport) {
const exportType =
sourceModule.exports.get(importName) ||
sourceModule.exports.get("default") ||
sourceModule.exports.get("*") ||
"unknown";
this.graph.connections.push({
from: resolvedPath,
to: modulePath,
exportName: importName,
importName: importName,
exportType: exportType,
});
}
if (verbose) {
console.error(`Connection check: ${resolvedPath} -> ${modulePath}`);
console.error(` Import: ${importName}, Has export: ${hasExport}`);
console.error(
` Source exports: ${Array.from(sourceModule.exports.entries())
.map(([name, type]) => `${type} ${name}`)
.join(", ")}`,
);
}
}
});
});
return this.graph;
}
analyzeWithDomains(
domainDepth: number = 1,
verbose: boolean = false,
): DependencyGraph {
this.analyze(verbose);
this.organizeDomains(domainDepth);
return this.graph;
}
private extractModuleInfo(
sourceFile: ts.SourceFile,
moduleInfo: ModuleInfo,
): void {
const visit = (node: ts.Node): void => {
// Extract exports with types
if (ts.isExportDeclaration(node)) {
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const exportName = element.propertyName?.text || element.name.text;
moduleInfo.exports.set(exportName, "export"); // Re-export type
});
}
if (!node.exportClause && node.moduleSpecifier) {
moduleInfo.exports.set("*", "namespace"); // export * from '...'
}
}
// Export default
if (node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) {
if (
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
) {
const nodeType = this.getNodeType(node);
moduleInfo.exports.set("default", nodeType);
}
}
// Named function exports
if (
ts.isFunctionDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (node.name) {
moduleInfo.exports.set(node.name.text, "function");
} else if (
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
) {
moduleInfo.exports.set("default", "function");
}
}
// Variable exports (export const, let, var)
if (
ts.isVariableStatement(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
node.declarationList.declarations.forEach((decl) => {
if (ts.isIdentifier(decl.name)) {
// Try to infer type from initializer
let varType = "variable";
if (decl.initializer) {
if (
ts.isArrowFunction(decl.initializer) ||
ts.isFunctionExpression(decl.initializer)
) {
varType = "function";
} else if (
ts.isNewExpression(decl.initializer) ||
ts.isClassExpression(decl.initializer)
) {
varType = "class";
} else if (ts.isObjectLiteralExpression(decl.initializer)) {
varType = "object";
} else if (ts.isArrayLiteralExpression(decl.initializer)) {
varType = "array";
}
}
moduleInfo.exports.set(decl.name.text, varType);
}
});
}
// Class exports
if (
ts.isClassDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (node.name) {
moduleInfo.exports.set(node.name.text, "class");
} else if (
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword)
) {
moduleInfo.exports.set("default", "class");
}
}
// Interface and type exports
if (
ts.isInterfaceDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (node.name) {
moduleInfo.exports.set(node.name.text, "interface");
}
}
if (
ts.isTypeAliasDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (node.name) {
moduleInfo.exports.set(node.name.text, "type");
}
}
// Enum exports
if (
ts.isEnumDeclaration(node) &&
node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
) {
if (node.name) {
moduleInfo.exports.set(node.name.text, "enum");
}
}
// Extract imports
if (
ts.isImportDeclaration(node) &&
node.moduleSpecifier &&
ts.isStringLiteral(node.moduleSpecifier)
) {
const modulePath = node.moduleSpecifier.text;
if (node.importClause) {
// Default import
if (node.importClause.name) {
moduleInfo.imports.set(node.importClause.name.text, modulePath);
}
// Named imports
if (
node.importClause.namedBindings &&
ts.isNamedImports(node.importClause.namedBindings)
) {
node.importClause.namedBindings.elements.forEach((element) => {
const importName =
element.propertyName?.text || element.name.text;
const localName = element.name.text;
moduleInfo.imports.set(importName, modulePath);
});
}
// Namespace import
if (
node.importClause.namedBindings &&
ts.isNamespaceImport(node.importClause.namedBindings)
) {
moduleInfo.imports.set("*", modulePath);
}
}
}
// Extract function calls
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
moduleInfo.functionCalls.add(node.expression.text);
}
if (
ts.isPropertyAccessExpression(node) &&
ts.isCallExpression(node.parent)
) {
const fullName = this.getFullPropertyName(node);
if (fullName) {
moduleInfo.functionCalls.add(fullName);
}
}
ts.forEachChild(node, visit);
};
ts.forEachChild(sourceFile, visit);
}
private getNodeType(node: ts.Node): string {
if (ts.isFunctionDeclaration(node)) return "function";
if (ts.isClassDeclaration(node)) return "class";
if (ts.isInterfaceDeclaration(node)) return "interface";
if (ts.isTypeAliasDeclaration(node)) return "type";
if (ts.isEnumDeclaration(node)) return "enum";
if (ts.isVariableStatement(node)) return "variable";
return "unknown";
}
private getFullPropertyName(
node: ts.PropertyAccessExpression,
): string | null {
const parts: string[] = [];
let current: ts.Node = node;
while (ts.isPropertyAccessExpression(current)) {
parts.unshift(current.name.text);
current = current.expression;
}
if (ts.isIdentifier(current)) {
parts.unshift(current.text);
return parts.join(".");
}
return null;
}
generateMermaidDiagram(useDomains: boolean = false): string {
if (!useDomains || this.graph.domains.size === 0) {
return this.generateBasicMermaidDiagram();
}
const lines = ["graph TD"];
// Add domain subgraphs
this.graph.domains.forEach((modules, domain) => {
const domainId = this.sanitizeForMermaid(domain);
lines.push(` subgraph ${domainId} ["${domain}"]`);
modules.forEach((modulePath) => {
const nodeId = this.sanitizeForMermaid(modulePath);
const displayName = path.basename(modulePath, path.extname(modulePath));
lines.push(` ${nodeId}["${displayName}"]`);
});
lines.push(` end`);
});
// Add edges
this.graph.connections.forEach((conn) => {
const fromId = this.sanitizeForMermaid(conn.from);
const toId = this.sanitizeForMermaid(conn.to);
const typePrefix = conn.exportType ? `${conn.exportType} ` : "";
const label =
conn.exportName !== conn.importName
? `${typePrefix}${conn.exportName}${conn.importName}`
: `${typePrefix}${conn.exportName}`;
lines.push(` ${fromId} -->|"${label}"| ${toId}`);
});
return lines.join("\n");
}
private generateBasicMermaidDiagram(): string {
const lines = ["graph TD"];
// Add nodes
this.graph.modules.forEach((moduleInfo, modulePath) => {
const nodeId = this.sanitizeForMermaid(modulePath);
const displayName = path.basename(modulePath, path.extname(modulePath));
lines.push(` ${nodeId}["${displayName}"]`);
});
// Add edges
this.graph.connections.forEach((conn) => {
const fromId = this.sanitizeForMermaid(conn.from);
const toId = this.sanitizeForMermaid(conn.to);
const typePrefix = conn.exportType ? `${conn.exportType} ` : "";
const label =
conn.exportName !== conn.importName
? `${typePrefix}${conn.exportName}${conn.importName}`
: `${typePrefix}${conn.exportName}`;
lines.push(` ${fromId} -->|"${label}"| ${toId}`);
});
return lines.join("\n");
}
generateDotDiagram(useDomains: boolean = false): string {
if (!useDomains || this.graph.domains.size === 0) {
return this.generateBasicDotDiagram();
}
const lines = ["digraph Dependencies {"];
lines.push(" rankdir=LR;");
lines.push(" node [shape=box, style=rounded];");
lines.push(" compound=true;"); // Allow edges between clusters
// Add domain clusters with styled labels
let clusterIndex = 0;
const domainNodes = new Map<string, string>(); // domain -> representative node
this.graph.domains.forEach((modules, domain) => {
const clusterId = `cluster_${clusterIndex++}`;
lines.push(` subgraph ${clusterId} {`);
lines.push(` label=<<B>${this.escapeHtml(domain)}</B>>;`); // Bold domain title
lines.push(" style=filled;");
lines.push(" color=lightgrey;");
lines.push(" labeljust=l;"); // Left-align cluster label
let firstNode: string | undefined;
modules.forEach((modulePath) => {
const nodeId = this.sanitizeForDot(modulePath);
const displayName = path.basename(modulePath, path.extname(modulePath));
lines.push(
` "${nodeId}" [label="${this.escapeHtml(displayName)}"];`,
);
if (!firstNode) firstNode = nodeId;
});
lines.push(" }");
if (firstNode) {
domainNodes.set(domain, firstNode);
}
});
// Add edges with styled labels
this.graph.connections.forEach((conn) => {
const fromId = this.sanitizeForDot(conn.from);
const toId = this.sanitizeForDot(conn.to);
// Create styled label with greyed-out type and normal symbol name
const styledLabel = this.createStyledEdgeLabel(
conn.exportType,
conn.exportName,
);
const fromDomain = this.getDomainFromPath(conn.from);
const toDomain = this.getDomainFromPath(conn.to);
if (fromDomain === toDomain) {
// Same domain - regular edge
lines.push(` "${fromId}" -> "${toId}" [label=<${styledLabel}>];`);
} else {
// Cross-domain edge - more subdued dark red
lines.push(
` "${fromId}" -> "${toId}" [label=<${styledLabel}>, style=bold, color="#8B0000"];`,
);
}
});
lines.push("}");
return lines.join("\n");
}
private generateBasicDotDiagram(): string {
const lines = ["digraph Dependencies {"];
lines.push(" rankdir=LR;");
lines.push(" node [shape=box, style=rounded];");
// Add nodes
this.graph.modules.forEach((moduleInfo, modulePath) => {
const nodeId = this.sanitizeForDot(modulePath);
const displayName = path.basename(modulePath, path.extname(modulePath));
lines.push(` "${nodeId}" [label="${this.escapeHtml(displayName)}"];`);
});
// Add edges with styled labels
this.graph.connections.forEach((conn) => {
const fromId = this.sanitizeForDot(conn.from);
const toId = this.sanitizeForDot(conn.to);
const styledLabel = this.createStyledEdgeLabel(
conn.exportType,
conn.exportName,
);
lines.push(` "${fromId}" -> "${toId}" [label=<${styledLabel}>];`);
});
lines.push("}");
return lines.join("\n");
}
private createStyledEdgeLabel(
exportType?: string,
exportName?: string,
): string {
if (!exportType || !exportName) {
return this.escapeHtml(exportName || "unknown");
}
// Style: greyed-out type, normal black symbol name
const greyType = `<FONT COLOR="#666666">${this.escapeHtml(exportType)}</FONT>`;
const blackName = `<FONT COLOR="#000000">${this.escapeHtml(exportName)}</FONT>`;
return `${greyType} ${blackName}`;
}
private escapeHtml(text: string): string {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
private getDomainFromPath(
modulePath: string,
domainDepth: number = 1,
): string {
const parts = modulePath.split("/").filter((p) => p.length > 0);
// Skip common prefixes like 'src'
let startIndex = 0;
if (parts[0] === "src" || parts[0] === "lib") {
startIndex = 1;
}
if (parts.length <= startIndex) {
return "root";
}
const domainParts = parts.slice(startIndex, startIndex + domainDepth);
return domainParts.join("/") || "root";
}
private organizeDomains(domainDepth: number = 1): void {
this.graph.domains.clear();
this.graph.modules.forEach((moduleInfo, modulePath) => {
const domain = this.getDomainFromPath(modulePath, domainDepth);
if (!this.graph.domains.has(domain)) {
this.graph.domains.set(domain, new Set());
}
this.graph.domains.get(domain)!.add(modulePath);
});
}
private sanitizeForMermaid(str: string): string {
return str.replace(/[^a-zA-Z0-9]/g, "_");
}
private sanitizeForDot(str: string): string {
// For node IDs, just remove problematic characters
return str.replace(/[^a-zA-Z0-9_]/g, "_");
}
generateReport(useDomains: boolean = false): string {
const lines = ["# TypeScript Dependency Analysis Report\n"];
lines.push(`## Summary`);
lines.push(`- Total modules: ${this.graph.modules.size}`);
lines.push(`- Total connections: ${this.graph.connections.length}`);
if (useDomains && this.graph.domains.size > 0) {
lines.push(`- Total domains: ${this.graph.domains.size}`);
}
lines.push("");
if (useDomains && this.graph.domains.size > 0) {
return this.generateDomainReport(lines);
} else {
return this.generateBasicReport(lines);
}
}
private generateDomainReport(lines: string[]): string {
// Domain overview
lines.push(`## Domain Overview\n`);
this.graph.domains.forEach((modules, domain) => {
lines.push(`### ${domain} (${modules.size} modules)`);
const moduleNames = Array.from(modules)
.map((m) => path.basename(m, path.extname(m)))
.join(", ");
lines.push(`Modules: ${moduleNames}\n`);
});
// Cross-domain dependencies
lines.push(`## Cross-Domain Dependencies\n`);
const crossDomainConnections = this.graph.connections.filter((conn) => {
const fromDomain = this.getDomainFromPath(conn.from);
const toDomain = this.getDomainFromPath(conn.to);
return fromDomain !== toDomain;
});
if (crossDomainConnections.length > 0) {
crossDomainConnections.forEach((conn) => {
const fromDomain = this.getDomainFromPath(conn.from);
const toDomain = this.getDomainFromPath(conn.to);
const fromFile = path.basename(conn.from, path.extname(conn.from));
const toFile = path.basename(conn.to, path.extname(conn.to));
const typePrefix = conn.exportType ? `${conn.exportType} ` : "";
lines.push(
`- **${fromDomain}** → **${toDomain}**: ${fromFile}.${typePrefix}${conn.exportName}${toFile}`,
);
});
} else {
lines.push("No cross-domain dependencies found.");
}
lines.push("");
// Domain details
lines.push(`## Domain Details\n`);
this.graph.domains.forEach((modules, domain) => {
lines.push(`### ${domain}\n`);
modules.forEach((modulePath) => {
const moduleInfo = this.graph.modules.get(modulePath)!;
const fileName = path.basename(modulePath, path.extname(modulePath));
lines.push(`#### ${fileName}`);
lines.push(`- Path: ${modulePath}`);
const exportsList = Array.from(moduleInfo.exports.entries())
.map(([name, type]) => `${type} ${name}`)
.join(", ");
lines.push(`- Exports: ${exportsList || "none"}`);
lines.push(
`- Imports: ${Array.from(moduleInfo.imports.keys()).join(", ") || "none"}`,
);
lines.push(
`- Function calls: ${Array.from(moduleInfo.functionCalls).join(", ") || "none"}\n`,
);
});
});
return lines.join("\n");
}
private generateBasicReport(lines: string[]): string {
lines.push(`## Modules\n`);
this.graph.modules.forEach((moduleInfo, modulePath) => {
const fileName = path.basename(modulePath, path.extname(modulePath));
lines.push(`### ${fileName}`);
lines.push(`- Path: ${modulePath}`);
const exportsList = Array.from(moduleInfo.exports.entries())
.map(([name, type]) => `${type} ${name}`)
.join(", ");
lines.push(`- Exports: ${exportsList || "none"}`);
lines.push(
`- Imports: ${Array.from(moduleInfo.imports.keys()).join(", ") || "none"}`,
);
lines.push(
`- Function calls: ${Array.from(moduleInfo.functionCalls).join(", ") || "none"}\n`,
);
});
return lines.join("\n");
}
}
// CLI Interface
function main() {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log(`
Usage: ts-analyzer <project-path> [options]
Options:
--format <mermaid|dot|report> Output format (default: mermaid)
--output <file> Output file (default: stdout)
--tsconfig <path> Path to tsconfig.json
--domains Group modules by domain/folder structure
--domain-depth <number> Domain grouping depth (default: 1)
--verbose Enable verbose logging
Examples:
ts-analyzer ./src --format mermaid > deps.mmd
ts-analyzer ./src --format dot --domains | dot -Tsvg > deps.svg
ts-analyzer ./src --format report --domains --domain-depth 2
ts-analyzer ./src --domains --verbose
Domain Examples:
src/fetch/api.ts, src/fetch/utils.ts → "fetch" domain
src/insights/charts/bar.ts → "insights" domain (depth=1)
src/insights/charts/bar.ts → "insights/charts" domain (depth=2)
DOT Styling Features:
- Domain titles in bold
- Symbol types in grey (function, class, interface, etc.)
- Symbol names in normal black
- Cross-domain dependencies highlighted
`);
process.exit(1);
}
const projectPath = args[0];
let format = "mermaid";
let outputFile: string | undefined;
let tsconfigPath: string | undefined;
let verbose = false;
let useDomains = false;
let domainDepth = 1;
for (let i = 1; i < args.length; i++) {
switch (args[i]) {
case "--format":
format = args[i + 1];
i++; // Skip next arg since we consumed it
break;
case "--output":
outputFile = args[i + 1];
i++; // Skip next arg since we consumed it
break;
case "--tsconfig":
tsconfigPath = args[i + 1];
i++; // Skip next arg since we consumed it
break;
case "--domain-depth":
domainDepth = parseInt(args[i + 1], 10);
if (isNaN(domainDepth) || domainDepth < 1) {
console.error("Domain depth must be a positive integer");
process.exit(1);
}
i++; // Skip next arg since we consumed it
break;
case "--domains":
useDomains = true;
break;
case "--verbose":
verbose = true;
break;
}
}
try {
const analyzer = new TypeScriptAnalyzer(projectPath, tsconfigPath);
if (useDomains) {
analyzer.analyzeWithDomains(domainDepth, verbose);
} else {
analyzer.analyze(verbose);
}
let output: string;
switch (format) {
case "mermaid":
output = analyzer.generateMermaidDiagram(useDomains);
break;
case "dot":
output = analyzer.generateDotDiagram(useDomains);
break;
case "report":
output = analyzer.generateReport(useDomains);
break;
default:
throw new Error(`Unknown format: ${format}`);
}
if (outputFile) {
fs.writeFileSync(outputFile, output);
console.log(`Output written to ${outputFile}`);
} else {
console.log(output);
}
} catch (error) {
console.error("Error:", error.message);
if (verbose) {
console.error(error.stack);
}
process.exit(1);
}
}
if (require.main === module) {
main();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment