An open API service indexing awesome lists of open source software.

https://github.com/matzehuels/stacktower

Visualize package dependencies as XKCD-style tower diagrams. Supports Python, Rust, JavaScript, Ruby, PHP, Java, and Go.
https://github.com/matzehuels/stacktower

dependencies golang graphs

Last synced: 28 days ago
JSON representation

Visualize package dependencies as XKCD-style tower diagrams. Supports Python, Rust, JavaScript, Ruby, PHP, Java, and Go.

Awesome Lists containing this project

README

          

# Stacktower

[![CI](https://github.com/matzehuels/stacktower/actions/workflows/ci.yml/badge.svg)](https://github.com/matzehuels/stacktower/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/matzehuels/stacktower/graph/badge.svg)](https://codecov.io/gh/matzehuels/stacktower)
[![Go Report Card](https://goreportcard.com/badge/github.com/matzehuels/stacktower)](https://goreportcard.com/report/github.com/matzehuels/stacktower)
[![Go Reference](https://pkg.go.dev/badge/github.com/matzehuels/stacktower.svg)](https://pkg.go.dev/github.com/matzehuels/stacktower)
[![Release](https://img.shields.io/github/v/release/matzehuels/stacktower)](https://github.com/matzehuels/stacktower/releases)
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)

Inspired by [XKCD #2347](https://xkcd.com/2347/), Stacktower renders dependency graphs as **physical towers** where blocks rest on what they depend on. Your application sits at the top, supported by libraries belowโ€”all the way down to that one critical package maintained by _some dude in Nebraska_.


FastAPI dependency tower

---


๐Ÿš€ Try the Web App ยท ๐Ÿ“– Read the Story

---

> โš ๏ธ **Note:** The ordering algorithms are still experimental and may not produce nicely stacked towers for projects with a large number of dependencies. Results can vary. We're actively working on improvements.

## Installation

### Homebrew (macOS/Linux)

```bash
brew install matzehuels/tap/stacktower
```

### Go

```bash
go install github.com/matzehuels/stacktower@latest
```

### From Source

```bash
git clone https://github.com/matzehuels/stacktower.git
cd stacktower
go build -o bin/stacktower ./cmd/stacktower
```

## Quick Start

```bash
# Render the included Flask example (XKCD-style tower is the default)
stacktower render examples/real/flask.json -o flask.svg
```

## Usage

Stacktower works in two stages: **parse** dependency data from package registries or manifest files, then **render** visualizations.

### Parsing Dependencies

The `parse` command auto-detects whether you're providing a package name or a manifest file:

```bash
stacktower parse [flags]
```

**Supported languages:** `python`, `rust`, `javascript`, `ruby`, `php`, `java`, `go`

#### From Package Registries

```bash
stacktower parse python fastapi -o fastapi.json # PyPI
stacktower parse rust serde -o serde.json # crates.io
stacktower parse javascript yargs -o yargs.json # npm
stacktower parse ruby rails -o rails.json # RubyGems
stacktower parse php monolog/monolog -o monolog.json # Packagist
stacktower parse java com.google.guava:guava -o guava.json # Maven Central
stacktower parse go github.com/gin-gonic/gin -o gin.json # Go Module Proxy
```

#### From Manifest Files (Auto-Detected)

```bash
stacktower parse python examples/manifest/poetry.lock -o deps.json
stacktower parse python examples/manifest/requirements.txt -o deps.json
stacktower parse rust examples/manifest/Cargo.toml -o deps.json
stacktower parse javascript examples/manifest/package.json -o deps.json
stacktower parse ruby examples/manifest/Gemfile -o deps.json
stacktower parse php examples/manifest/composer.json -o deps.json
stacktower parse java examples/manifest/pom.xml -o deps.json
stacktower parse go examples/manifest/go.mod -o deps.json
```

When the argument exists on disk or matches a known manifest filename, Stacktower automatically parses it as a manifest.

The project name (root node) is auto-detected from the manifest or a sibling file:

- **Cargo.toml**: `[package].name`
- **go.mod**: `module` directive
- **package.json**: `name` field
- **composer.json**: `name` field
- **pom.xml**: `groupId:artifactId`
- **poetry.lock / requirements.txt**: `pyproject.toml` (sibling)
- **Gemfile**: `*.gemspec` (sibling)

Use `--name` to override the auto-detected name:

```bash
stacktower parse python requirements.txt --name="my-project" -o deps.json
stacktower parse ruby Gemfile -n my-rails-app -o deps.json
```

#### Metadata Enrichment

By default, Stacktower enriches packages with GitHub metadata (stars, maintainers, last commit) for richer visualizations. Set `GITHUB_TOKEN` to enable this:

```bash
export GITHUB_TOKEN=your_token
stacktower parse python fastapi -o fastapi.json

# Skip enrichment if you don't have a token
stacktower parse python fastapi --skip-enrich -o fastapi.json
```

### Rendering

The `render` command generates visualizations from parsed JSON graphs:

```bash
stacktower render [flags]
```

This is a shortcut that combines `layout` and `visualize` in one step. For more control, you can run them separately:

```bash
# Two-step workflow with intermediate layout
stacktower layout examples/real/flask.json -o flask.layout.json
stacktower visualize flask.layout.json -o flask.svg
```

#### Visualization Types

```bash
# Hand-drawn XKCD-style tower (default)
stacktower render examples/real/flask.json -o flask.svg

# Disable hand-drawn effects for a cleaner look
stacktower render examples/real/serde.json --style simple --randomize=false --popups=false -o serde.svg

# Traditional node-link diagram (uses Graphviz DOT)
stacktower render examples/real/yargs.json -t nodelink -o yargs.svg
```

#### Output Formats

```bash
# SVG output (default)
stacktower render examples/real/flask.json -o flask.svg

# JSON layout export (for external tools or re-rendering)
stacktower render examples/real/flask.json -f json -o flask.json

# PDF output
stacktower render examples/real/flask.json -f pdf -o flask.pdf

# PNG output (2x scale by default)
stacktower render examples/real/flask.json -f png -o flask.png

# Multiple formats at once (outputs flask.svg, flask.json, flask.pdf)
stacktower render examples/real/flask.json -f svg,json,pdf -o flask
```

Output path behavior:

- **No `-o`**: Derives from input (`input.json` โ†’ `input.`)
- **Single format**: Uses exact path (`-o out.svg` โ†’ `out.svg`)
- **Multiple formats**: Strips extension, adds format (`-o out -f svg,json` โ†’ `out.svg`, `out.json`)

> **Note:** PDF and PNG output requires [librsvg](https://wiki.gnome.org/Projects/LibRsvg):
>
> - macOS: `brew install librsvg`
> - Linux: `apt install librsvg2-bin`

### Included Examples

The repository ships with pre-parsed graphs so you can experiment immediately:

```bash
# Real packages with full metadata (XKCD-style by default)
stacktower render examples/real/flask.json -o flask.svg
stacktower render examples/real/serde.json -o serde.svg
stacktower render examples/real/yargs.json -o yargs.svg

# With Nebraska guy ranking
stacktower render examples/real/flask.json --nebraska -o flask.svg

# Synthetic test cases
stacktower render examples/test/diamond.json -o diamond.svg
```

## Options Reference

### Global Options

| Flag | Description |
| ----------------- | -------------------------------------------------------- |
| `-v`, `--verbose` | Enable debug logging (search space info, timing details) |

### Parse Options

| Flag | Description |
| ---------------- | -------------------------------------------------------------------------- |
| `-o`, `--output` | Output file (stdout if empty) |
| `-n`, `--name` | Project name for manifest parsing (auto-detected from manifest if not set) |
| `--max-depth N` | Maximum dependency depth (default: 10) |
| `--max-nodes N` | Maximum packages to fetch (default: 5000) |
| `--skip-enrich` | Skip metadata enrichment (GitHub descriptions, etc.) |
| `--no-cache` | Disable caching |

### Render Options

| Flag | Description |
| ---------------- | --------------------------------------------------------------------------------------------------------------------- |
| `-o`, `--output` | Output file or base path for multiple formats |
| `-t`, `--type` | Visualization type: `tower` (default), `nodelink` |
| `-f`, `--format` | Output format(s): `svg` (default), `json`, `pdf`, `png` (comma-separated) |
| `--normalize` | Apply graph normalization: break cycles, remove transitive edges, assign layers, subdivide long edges (default: true) |

#### Tower Options

| Flag | Description |
| --------------------------------- | --------------------------------------------------------------------- |
| `--width N` | Frame width in pixels (default: 800) |
| `--height N` | Frame height in pixels (default: 600) |
| `--style handdrawn\|simple` | Visual style (default: handdrawn) |
| `--randomize` | Vary block widths to visualize load-bearing structure (default: true) |
| `--merge` | Merge subdivider blocks into continuous towers (default: true) |
| `--popups` | Enable hover popups with package metadata (default: true) |
| `--nebraska` | Show "Nebraska guy" maintainer ranking panel |
| `--edges` | Show dependency edges as dashed lines |
| `--ordering optimal\|barycentric` | Crossing minimization algorithm (default: optimal) |
| `--ordering-timeout N` | Timeout for optimal search in seconds (default: 60) |
| `--no-cache` | Disable caching |

## JSON Format

The render layer accepts a simple JSON format, making it easy to visualize **any** directed graphโ€”not just package dependencies. You can hand-craft graphs for component diagrams, callgraphs, or pipe output from other tools.

### Minimal Example

```json
{
"nodes": [{ "id": "app" }, { "id": "lib-a" }, { "id": "lib-b" }],
"edges": [
{ "from": "app", "to": "lib-a" },
{ "from": "lib-a", "to": "lib-b" }
]
}
```

### Required Fields

| Field | Type | Description |
| -------------- | ------ | ------------------------------------------- |
| `nodes[].id` | string | Unique node identifier (displayed as label) |
| `edges[].from` | string | Source node ID |
| `edges[].to` | string | Target node ID |

### Optional Fields

| Field | Type | Description |
| -------------- | ------ | ------------------------------------------------------ |
| `nodes[].row` | int | Pre-assigned layer (computed automatically if omitted) |
| `nodes[].kind` | string | Internal use: `"subdivider"` or `"auxiliary"` |
| `nodes[].meta` | object | Freeform metadata for display features |

### Recognized `meta` Keys

These keys are read by specific render flags. All are optionalโ€”missing keys simply disable the corresponding feature.

| Key | Type | Used By |
| ------------------- | ------------- | ------------------------------------------ |
| `repo_url` | string | Clickable blocks, `--popups`, `--nebraska` |
| `repo_stars` | int | `--popups` |
| `repo_owner` | string | `--nebraska` |
| `repo_maintainers` | []string | `--nebraska` |
| `repo_last_commit` | string (date) | `--popups`, brittle detection |
| `repo_last_release` | string (date) | `--popups` |
| `repo_archived` | bool | `--popups`, brittle detection |
| `summary` | string | `--popups` (fallback: `description`) |

The `--detailed` flag (node-link only) displays **all** meta keys in the node label.

## How It Works

1. **Parse** โ€” Fetch package metadata from registries or local manifest files
2. **Reduce** โ€” Remove transitive edges to show only direct dependencies
3. **Layer** โ€” Assign each package to a row based on its depth
4. **Order** โ€” Minimize edge crossings using branch-and-bound with PQ-tree pruning
5. **Layout** โ€” Compute block widths proportional to downstream dependents
6. **Render** โ€” Generate output in SVG, JSON, PDF, or PNG format

The ordering step is where the magic happens. Stacktower uses an optimal search algorithm that guarantees minimum crossings for small-to-medium graphs. For larger graphs, it gracefully falls back after a configurable timeout.

## GitHub Authentication

For parsing repositories directly from GitHub, you can authenticate using the device flow:

```bash
# Login with GitHub (opens browser for device authorization)
stacktower github login

# Check current session
stacktower github whoami

# Logout
stacktower github logout

# Parse a manifest from a GitHub repository
stacktower parse github owner/repo -o deps.json
```

## Environment Variables

| Variable | Description |
| -------------- | ------------------------------------------------ |
| `GITHUB_TOKEN` | GitHub API token for metadata enrichment |
| `GITLAB_TOKEN` | GitLab API token for metadata enrichment |

## Caching

HTTP responses are cached in `~/.cache/stacktower/` with a 24-hour TTL. Use `--no-cache` to disable caching for a single request.

```bash
# Clear the entire cache
stacktower cache clear

# Show cache directory path
stacktower cache path
```

## Using as a Library

Stacktower can be used as a Go library for programmatic graph visualization.

```go
import (
"github.com/matzehuels/stacktower/pkg/core/dag"
"github.com/matzehuels/stacktower/pkg/core/dag/transform"
"github.com/matzehuels/stacktower/pkg/core/render/tower/layout"
"github.com/matzehuels/stacktower/pkg/core/render/tower/sink"
)

// Build a graph
g := dag.New(nil)
g.AddNode(dag.Node{ID: "app", Row: 0})
g.AddNode(dag.Node{ID: "lib", Row: 1})
g.AddEdge(dag.Edge{From: "app", To: "lib"})

// Normalize and render
transform.Normalize(g)
l := layout.Build(g, 800, 600)
svg := sink.RenderSVG(l, sink.WithGraph(g), sink.WithPopups())
```

๐Ÿ“š **[Full API documentation on pkg.go.dev](https://pkg.go.dev/github.com/matzehuels/stacktower)**

Key packages:

- [`pkg/core/dag`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag) โ€” DAG data structure and crossing algorithms
- [`pkg/core/dag/transform`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/dag/transform) โ€” Graph normalization pipeline
- [`pkg/core/render/tower`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/render/tower) โ€” Layout, ordering, and rendering
- [`pkg/core/deps`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/core/deps) โ€” Dependency resolution from registries
- [`pkg/pipeline`](https://pkg.go.dev/github.com/matzehuels/stacktower/pkg/pipeline) โ€” Complete parse โ†’ layout โ†’ render pipeline

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on adding new languages, manifest parsers, or output formats.

## Development

```bash
make install-tools # Install required tools (golangci-lint, goimports, govulncheck)
make check # Run all CI checks locally (fmt, lint, test, vuln)
make build # Build binary to bin/stacktower
```

| Command | Description |
| --------------- | ------------------------------------------ |
| `make check` | Format, lint, test, vulncheck (same as CI) |
| `make fmt` | Format code with gofmt and goimports |
| `make lint` | Run golangci-lint |
| `make test` | Run tests with race detector |
| `make cover` | Run tests with coverage report |
| `make vuln` | Check for known vulnerabilities |
| `make e2e` | Run end-to-end tests |
| `make snapshot` | Build release locally (no publish) |

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/).

## Learn More

- ๐Ÿ“– **[stacktower.io](https://www.stacktower.io)** โ€” Interactive examples and the full story behind tower visualizations
- ๐Ÿ› **[Issues](https://github.com/matzehuels/stacktower/issues)** โ€” Bug reports and feature requests

## License

Apache-2.0