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
💡 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.
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"
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/**"
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/**"
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/**"
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/"
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)