DEV Community

Cover image for Dep Tree - a tool for rendering and linting your project's dependency tree in the terminal
Gabriel
Gabriel

Posted on

Dep Tree - a tool for rendering and linting your project's dependency tree in the terminal

Dep Tree

https://github.com/gabotechs/dep-tree

Coding is a team sport, and team members should agree on conventions and strategies for taking projects across the line.

For example, team members may agree on a specific code style, and configure linters for enforcing it, that way no one can’t get their code merged without first be compliant with the styling rules.

Team members could also choose to agree on strategies for managing dependencies, something like “files in folder X” should never depend on “anything under folder Y”. A clean dependency management is crucial for a project’s maintainability and scalability in the long term, but there is not that many tools for enforcing dependency higiene as there is for code style higiene.

Dep Tree aims to solve this, not only by allowing developers to visualize in an interactive way their project’s dependency tree, but also by providing a tool for enforcing some user-defined dependency rules. Something like a linter, but for dependencies.

Dependency tree visualization

The first step for knowing how messy a project is, is by graphically visualizing it. Dep Tree allows rendering an interactive dependency tree in the terminal, which is super useful for understanding what pieces of a project depend on what, and what other things are correctly decoupled.

Take this project of mine as an example: https://github.com/gabotechs/react-gcode-viewer

That project is just a very simple React component library, and as usually, it has an entrypoint in src/index.ts. A very important thing to note, is that there is always an entrypoint, either the index file used for exposing a library, or the executable file with the program’s main function.

Running dep-tree render src/index.ts under that project’s root folder will render something like this in the terminal:

index.ts                                                                        
│                                                                               
└▷GCodeViewer/GCodeViewer.tsx                                                   
  │                                                                             
  ├▷GCodeViewer/GCodeModel.tsx                                                  
  └▷│GCodeViewer/gcode/reader/UrlReader.ts                                      
    ││                                                                          
    ├│▷GCodeViewer/GCodeLines.tsx                                               
    ├│▷│GCodeViewer/SceneElements/Camera.tsx                                    
    ├│▷│GCodeViewer/SceneElements/Floor.tsx                                     
    ├│▷│GCodeViewer/SceneElements/OrbitControls.tsx                             
    ├│▷│GCodeViewer/gcode/GCode.ts                                              
    │└▷││GCodeViewer/gcode/reader/base.ts                                       
    │  ││                                                                       
    │  │├─▷GCodeViewer/gcode/parser.ts                                          
    │  ││  │                                                                    
    └──┴┴──┴▷GCodeViewer/gcode/types.ts
Enter fullscreen mode Exit fullscreen mode

💡 Dep Tree does not check third-party dependencies, it will only take into account the source code of the project, ignoring any dependency to a external library.

This is just copy-pasted from a terminal, but the truth is that users can navigate through the entries using the keyboard, and select specific entries for rendering their isolated dependency trees.

Demo Gif

Dependency rules check

Once users are already aware of how messy their project’s dependencies are, they may choose to fix it and enforce some dependency rules. In the same way that developers configure linters for checking that new code matches the expected style, they can define rules to ensure that new code does not introduce dependencies between parts of the application that are meant to be decoupled.

In Dep Tree, this rules are defined in a .dep-tree.yml file in the root of the project with the following structure:

entrypoints:
  - src/index.ts
allow:
  "this-folder-can-only-depend-on.../**":
    - "this/*.ts"
    - "and-this/*.ts"
deny:
  "this-one-cannot-depend-on.../**":
    - "not-this/*.ts"
    - "neither-this/*.ts"
Enter fullscreen mode Exit fullscreen mode

Some real life examples:

⚛ React pages and components

Imagine a React application, with a pages folder and a components folder. Probably, files under pages will use the reusable components defined in components for building whole pages, but components are just isolated testable components that should not depend on page-specific details:

entrypoints:
  - src/app.tsx
deny:
  "src/components/**/*.tsx":
    - "src/pages/**"
Enter fullscreen mode Exit fullscreen mode

running dep-tree check with this .dep-tree.yml file would fail if any component tries to depend on something that lives under the pages folder.

🔧 The classic utils folder

Who hasn’t dealt with this? a project that starts being a small thing, with some innocent helper functions that are placed under a utils folder. Then the project keeps growing, every new addition has its own “util” function or class, and because of the vague and generic meaning of the word “util”, everything can be considered a “util”. Some months later half of the project ends up living in a utils folder.

entrypoints:
  - src/main.ts
allow:
  "src/utils/**":
    - "src/utils/**"
Enter fullscreen mode Exit fullscreen mode

This config will limit files under src/utils to only import other files under src/utils. That way a “util” can’t depend on parts of the application with business logic important for a specific functionality, developers are forced to colocate things that are designed for functionality X under the folder designed for functionality X, and the utils folder is left just for some generic and basic helper functions.

🔄 Dependency inversion

Last letter of the SOLID principles and one of the most powerful tools in software development, but lets directly dive-in in a real world example:

An application that is in charge of managing users, that uses MySQL as a database, will probably have some MySQL client code under src/mysql-client, and some user management code under src/users. As the application is using MySQL for persisting user information, then the code under src/users will depend on src/mysql-client for storing the users.

The src/users high level module is depending on the src/mysql-client low level module, which has some serious impact in testability in isolation of infrastructure-specific details, or in other words, not needing a whole MySQL database running just for testing src/users.

Dependency inversion can help here by breaking the dependency between both modules, putting an interface in between, and making src/mysql-client depend on that interface. But explaining how dependency inversion work is out of the scope here, so let's see directly how Dep Tree can help here:

entrypoints:
  - src/main.ts
deny:
  "src/users/**":
    - "src/mysql-client/**"
Enter fullscreen mode Exit fullscreen mode

This will enforce the src/users module to not depend on MySQL-specific details.

Even better, if all the infrastructure-specific code is gathered under a infra package, the dependency banning can go even further:

entrypoints:
  - src/main.ts
deny:
  "src/users/**":
    - "infra/**"
Enter fullscreen mode Exit fullscreen mode

Summary

Dep Tree aims to be another automated tool for helping in the maintainability of medium/large code bases. It helps to understand the dependency flow of an application and provides tools for validating it against the rules defined in a configuration file.

If you find it helpful, feel free to stop by the source repository https://github.com/gabotechs/dep-tree and give it a try, feedback is much appreciated!

Top comments (0)