Skip to content

Dependency Graph

Dependency Graph is a lightweight, composable set of Bash scripts that turns an npm monorepo’s internal dependency relationships into a visual Graphviz diagram. It was originally built to tame the complexity of a large audio-platform monorepo (ECAS) with dozens of interdependent packages — giving the team an at-a-glance picture of how everything fits together and catching missing package.json entries before they cause runtime errors.

In a monorepo with many packages it becomes surprisingly easy to import a dependency in source code without adding it to the package’s package.json. Everything works locally because hoisted node_modules hide the mistake — until a CI build or a consumer of the package hits a missing-dependency error. On top of that, understanding which packages depend on which quickly becomes impossible once you pass a dozen or so packages.

The toolchain is a Unix-style pipeline of small, single-purpose scripts chained together by main.sh:

Terminal window
# 1. Walk every package.json, extract matching deps
./find_dependencies.sh "$monorepo/packages/" deps.txt "ecas"
# 2. Convert the edge list into a Graphviz DOT file
./generate_dot_file.sh deps.txt graph.dot
# 3. Render the DOT file to a PNG
./generate_png.sh graph.dot graph.png

Each step produces a human-readable intermediate file, so you can inspect or transform the data at any point.

process_package.sh is where the clever part lives. It builds a single jq filter on the fly that merges dependencies and devDependencies, then selects only entries whose key matches a user-supplied regex:

Terminal window
str="select(.dependencies != null or .devDependencies != null) |
.name as \$parent |
(.dependencies // {}) + (.devDependencies // {}) |
to_entries[] |
select(.key | test(\"${match}\"; \"i\")) |
\"\(\$parent) \(.key)\""
jq -r "${str}" "$2"

This means you can easily scope the graph to just your own packages (ecas), or use a more advanced pattern like ^(?!.*ecas-docs).*ecas.*$ to exclude documentation packages.

A separate check-deps.sh script statically analyses TypeScript/TSX source files to find every import ... from "..." statement, extracts the package name (handling scoped @org/pkg packages correctly), and cross-references against the declared dependencies. Any missing entry is reported with colour-coded terminal output:

ERROR: [ecas-engine] Missing imports:
@ecas/core some-unlisted-lib
SUCCESS: [ecas-player]

check-all.sh wraps this into a single command that iterates every package in the monorepo — ideal for a CI gate.

  • Visual Dependency Graph — Generates a PNG diagram of inter-package dependencies via Graphviz DOT
  • Regex-Based Filtering — Scope the graph to packages matching any regex, including negative lookaheads
  • Unlisted-Import Detection — Statically parses TS/TSX imports and flags anything not declared in package.json
  • Scoped Package Awareness — Correctly handles @org/package style npm scopes when checking imports
  • UUID-Based Output — Each run produces uniquely named intermediate files, so parallel runs never collide
  • Composable Pipeline — Each script does one thing and can be used independently or swapped out
LanguageBash
Graph RenderingGraphviz (dot)
JSON Processingjq
Static AnalysisAWK + grep over TS/TSX sources
LicenseMIT
Terminal window
# Install prerequisites (macOS)
brew install graphviz jq
# Generate a dependency graph for all "ecas" packages
bash main.sh "../ecas" "./output" "ecas"
# Check a single package for unlisted imports
bash check-deps.sh "../ecas/packages/ecas-engine"
# Check every package in the monorepo
bash check-all.sh "../ecas/packages"

The source code is available on the project’s GitHub repository.