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.
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:
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/**"
dep-tree check with this
.dep-tree.yml file would fail if any
component tries to depend on something that lives under the
🔧 The classic
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
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.
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
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/**"
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)