{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreigmc4fn3byc6u5an2oyb5rq3ssqdaoeav3j3ah2wr7ca4sodpuav4",
"uri": "at://did:plc:fdfg7uwpthbb4zdam6e2xtvn/app.bsky.feed.post/3mnfkcmob7ff3"
},
"description": "Create a tool that analyzes NPM package dependencies and generates a visual ASCII representation showing which versions are compatible with each other, detecting circular dependencies and version conflicts across the entire dependency tree. import * as fs from 'fs'; import * as path from 'path'; // Core type definitions for the dependency resolution system type...",
"path": "/build-a-recursive-dependency-graph-visualizer-for-npm-package-compatibility/",
"publishedAt": "2026-06-03T16:38:05.000Z",
"site": "at://did:plc:fdfg7uwpthbb4zdam6e2xtvn/site.standard.publication/3mmcvowa3clzx",
"tags": [
"TypeScript",
"AI Code Review"
],
"textContent": "Create a tool that analyzes NPM package dependencies and generates a visual ASCII representation showing which versions are compatible with each other, detecting circular dependencies and version conflicts across the entire dependency tree. import * as fs from 'fs'; import * as path from 'path'; // Core type definitions for the dependency resolution system type PackageName = string & { readonly __brand: 'PackageName' }; type VersionString = string & { readonly __brand: 'VersionString' }; type DependencyHash = string & { readonly __brand: 'DependencyHash' }; interface PackageMetadata { name: PackageName; version: VersionString; dependencies: Map<PackageName, VersionString>; devDependencies: Map<PackageName, VersionString>; } interface CompatibilityNode { package: PackageMetadata; children: CompatibilityNode[]; visited: boolean; depth: number; } class DependencyGraphVisualizer { private nodeCache: Map<DependencyHash, CompatibilityNode> = new Map(); private circularDependencies: Set<DependencyHash> = new Set(); private visualizationBuffer: string[] = []; // Constructor initializes the visualizer with empty state constructor(private packageRoot: string) { this.validatePackageRoot(); } // Validates that the package root directory exists private validatePackageRoot(): void { if (!fs.existsSync(this.packageRoot)) { throw new Error(`Package root does not exist: ${this.packageRoot}`); } } // Creates a branded type for package names to ensure type safety private createPackageName(name: string): PackageName { return name as PackageName; } // Creates a branded type for version strings to ensure type safety private createVersionString(version: string): VersionString { return version as VersionString; } // Generates a unique hash for dependency tracking purposes private generateDependencyHash(name: PackageName, version: VersionString): DependencyHash { return `${name}@${version}` as DependencyHash; } // Loads package.json from the specified directory and parses it private async loadPackageJson(directory: string): Promise<PackageMetadata> { const packageJsonPath = path.join(directory, 'package.json'); const rawContent = fs.readFileSync(packageJsonPath, 'utf-8'); const parsed = JSON.parse(rawContent); return { name: this.createPackageName(parsed.name || 'unnamed'), version: this.createVersionString(parsed.version || '0.0.0'), dependencies: new Map(Object.entries(parsed.dependencies || {})) as Map<PackageName, VersionString>, devDependencies: new Map(Object.entries(parsed.devDependencies || {})) as Map<PackageName, VersionString>, }; } // Recursively builds the dependency tree with cycle detection private async buildDependencyTree( metadata: PackageMetadata, depth: number = 0, visitedPath: Set<DependencyHash> = new Set(), ): Promise<CompatibilityNode> { const hash = this.generateDependencyHash(metadata.name, metadata.version); // Check for circular dependency pattern before processing if (visitedPath.has(hash)) { this.circularDependencies.add(hash); return { package: metadata, children: [], visited: true, depth, }; } // Return cached node if available to avoid redundant processing if (this.nodeCache.has(hash)) { return this.nodeCache.get(hash)!; } const newVisitedPath = new Set(visitedPath); newVisitedPath.add(hash); const node: CompatibilityNode = { package: metadata, children: [], visited: false, depth, }; // Process child dependencies with proper error handling for (const [depName, depVersion] of metadata.dependencies) { try { // Attempt to load child package metadata (this may fail in real scenarios) const childMetadata = await this.loadPackageJson(path.join(this.packageRoot, 'node_modules', depName as string)); const childNode = await this.buildDependencyTree(childMetadata, depth + 1, newVisitedPath); node.children.push(childNode); } catch (error) { // Gracefully handle missing or inaccessible packages console.warn(`Failed to load dependency: ${depName}`); } } this.nodeCache.set(hash, node); return node; } // Generates ASCII tree visualization with proper indentation private visualizeNode(node: CompatibilityNode, prefix: string = ''): void { const isCircular = this.circularDependencies.has(this.generateDependencyHash(node.package.name, node.package.version)); const marker = isCircular ? '[CIRCULAR]' : ''; this.visualizationBuffer.push(`${prefix}${node.package.name}@${node.package.version} ${marker}`); // Iterate through children and format them properly for (let i = 0; i < node.children.length; i++) { const isLast = i === node.children.length - 1; const newPrefix = prefix + (isLast ? ' ' : ' '); this.visualizeNode(node.children[i], newPrefix); } } // Main public method to execute the full visualization pipeline public async visualize(): Promise<string> { try { const rootMetadata = await this.loadPackageJson(this.packageRoot); const tree = await this.buildDependencyTree(rootMetadata); this.visualizeNode(tree); return this.visualizationBuffer.join('n'); } catch (error) { throw new Error(`Visualization failed: ${error}`); } } } // Execute the visualizer (async () => { const visualizer = new DependencyGraphVisualizer(process.cwd()); const output = await visualizer.visualize(); console.log(output); })().catch(console.error); Code Review 1. Lines 4-7. Using branded types for PackageName, VersionString, and DependencyHash adds zero runtime value and makes the code harder to read. This is the kind of TypeScript 'security theater' that looks good in architecture docs but inflates the codebase. The type system already tracks these, and the & { readonly __brand: 'PackageName' } pattern adds cognitive overhead without preventing actual bugs. 2. Lines 24-26. The constructor validation is reasonable, but calling it synchronously in the constructor while the class has async methods elsewhere creates an inconsistent API. Either make the constructor async or move validation into the visualize() method. This mixing of sync and async patterns is a code smell. 3. Lines 33-40. The createPackageName() and createVersionString() wrapper methods don't do anything except cast a string to a branded type. These could be simple helper functions or just as PackageName inline, but wrapping them in methods and passing them through the entire codebase makes tracing data flow unnecessarily difficult. 4. Lines 50-51. The method signature says it's async but it's using synchronous fs.readFileSync(). This defeats the entire purpose of marking the function as async. Either make it genuinely asynchronous with fs.promises.readFile() or remove the async keyword. This confusion will break any attempt to parallelize package loading. 5. Lines 57-58. The dependencies and devDependencies are being cast to Map<PackageName, VersionString>, but Object.entries() returns [string, string][]. The type casting here is lying to the type system. Either validate the object structure properly or accept the more honest Map<string, string> type. 6. Lines 77-96. The buildDependencyTree() method has three parameters with the third being optional and progressively deeper nesting. This is classic callback hell in disguise. By the time you're three levels deep, the visitedPath parameter tracking becomes fragile. A graph-walking state machine or iterative approach with an explicit stack would be more maintainable. 7. Lines 117-122. The error handling for missing packages just logs a warning and continues. If a core dependency fails to load, silently skipping it will produce a misleading visualization. Either fail fast or make it explicit in the output what packages couldn't be resolved. Hiding errors like this makes debugging dependency issues nearly impossible. 8. Lines 142-145. The main() function at the bottom catches all errors and logs them, but this is already handled inside visualize() with a rethrown error. The double error handling adds nothing and obscures which layer actually failed. Remove one layer of exception handling or make each one do something specific.",
"title": "Build a Recursive Dependency Graph Visualizer for NPM Package Compatibility"
}