On a large Angular workspace at work, I needed to know what actually depended on a shared service before I touched it. grep finds text, not relationships. The routing config tells you what loads, not what it pulls in. And most "architecture diagram" tools want your code in the cloud, or want you to hand-maintain a diagram that's stale within a week.
So I built ng-loom — a CLI that statically analyzes an Angular workspace and spits out a single, self-contained HTML file with the whole dependency graph in it. No server, no account, no uploading your source anywhere. You run it, you get a report, you open it in a browser.
What it actually does
Point it at an Angular CLI project:
npx ng-loom ./path/to/angular-project --out report.html
and it walks the workspace with ts-morph, finds every component, directive, pipe, service, and NgModule, and maps out how they connect: standalone imports, NgModule imports/exports/declarations, constructor DI and inject() calls, hostDirectives, lazy routes via loadChildren/loadComponent. Template usage isn't regex-matched against your HTML — it runs your actual templates through @angular/compiler's real parser, so @if/@for/@switch/@defer blocks, structural directives, and attribute selectors all resolve correctly instead of getting silently missed.
Every edge in the graph carries the file, line, and column it came from, plus a vscode:// link straight to the source. So when the report tells you ArticleComponent injects UserService, you're not taking its word for it — you can jump to the exact line.
The report itself has a few views:
- A folder/package overview that treemaps your codebase into clusters, so you can see at a glance where the size and complexity actually live before drilling into a single file.
- A node focus view with a Details panel — imports, injected services, host directives, template usages, all with source snippets.
- Trace relation, which answers "how does A actually reach B?" — it finds every path between two artifacts, not just the first one it stumbles on, which matters more than you'd think once you're a few layers of indirection deep.
Here's what that looks like against a real app (ngx-admin, 294 nodes, 340 edges):
Why a static HTML file
This was the one design decision I didn't waver on. I didn't want a dashboard, a login, or a background service watching your repo. I wanted something you could generate, drop in a PR description, email to a teammate, or just keep around as a snapshot of "this is what the app looked like in March." A plain HTML file with the graph data embedded in it does all of that without asking anything of the person opening it.
The tradeoff is that it's a snapshot, not a live view — regenerate it after a big refactor and it's current again, but it won't update itself while you work. That's a fair trade for something that requires zero setup on the receiving end.
What's there and what isn't
Being upfront about the current state: pipe usage in templates ({{ value | somePipe }}) isn't tracked as an edge yet, class-binding selectors ([ngClass], [class.x]) aren't matched, and there's no Nx monorepo support — it expects a standard Angular CLI tsconfig.app.json/angular.json layout. Route parsing covers the common static patterns, not every dynamic route construction you could write. These are documented, not hidden, and they're the next things on the list.
What is there: real Angular template AST parsing (not regex), signal-based input()/output()/model() API extraction with aliases, the experimental @Service() decorator alongside @Injectable(), and a Playwright test suite covering the actual generated report in a real browser, not just unit tests against the analyzer.
Try it
npx ng-loom ./path/to/your-angular-app --out report.html
It's on npm as ng-loom, and the source is on GitHub at xonaib/ng-loom. If you run it against something interesting — or it chokes on your workspace layout — I'd genuinely like to hear about it. Open an issue, or just tell me what broke.


Top comments (0)