ngx-phantom
v1.0.0
Published
Dead code eliminator for Angular monorepos — finds every exported symbol with zero consumers across your entire workspace
Maintainers
Readme
ngx-phantom
Dead code eliminator for Angular monorepos.
Statically analyzes your entire dependency graph and reports every publicly exported symbol that has zero consumers across the workspace. Then offers an auto-prune mode to remove them from barrel files.
Written in Zig — scans a 150-library monorepo in under 300ms.
Installation
npm install -g ngx-phantom
# or run without installing
npx ngx-phantom analyzeRequirements
Your monorepo must have a tsconfig.base.json (NX) or tsconfig.json at the root with compilerOptions.paths mapping library names to entry files:
// tsconfig.base.json
{
"compilerOptions": {
"paths": {
"@myorg/ui": ["libs/ui/src/index.ts"],
"@myorg/core": ["libs/core/src/public-api.ts"]
}
}
}Paths that point to a dist/ directory are automatically resolved to the source src/index.ts or src/public-api.ts sibling.
Commands
analyze — report dead exports
ngx-phantom analyze [path]Scans all .ts files in the workspace and reports every symbol that is publicly exported from a library barrel but never imported by any consumer.
ngx-phantom ▸ Analyze
──────────────────────────────────────────────────
Discovering workspace... found 24 libraries
Analyzing...
Done in 0.32s
Found 12 dead export(s) across 5 libraries:
@myorg/sort-pipe (1 dead)
✗ SortPipe libs/sort-pipe/src/lib/sort.pipe.ts:1
@myorg/button (1 dead)
✗ ButtonType libs/button/src/lib/models/button-type.ts:3
...
87 exports analyzed across 24 libraries
Run with --prune to automatically remove dead exports from barrel files.Flags
| Flag | Default | Description |
|---|---|---|
| --root | . | Workspace root (where tsconfig.base.json lives) |
| --format | text | Output format: text or json |
| --exclude-tests | false | Exclude *.spec.ts files from consumer analysis |
| --workers | 2×CPU | Number of parallel file-scan workers |
| --fail | false | Exit with code 1 if dead exports are found (CI mode) |
| --verbose | false | Print resolved library paths and worker count |
JSON output
ngx-phantom analyze --format=json{
"summary": {
"totalDeadExports": 12,
"totalExports": 87,
"totalLibraries": 24,
"skippedLibraries": ["@myorg/utils"]
},
"deadExports": [
{
"symbol": "SortPipe",
"library": "@myorg/sort-pipe",
"sourceFile": "libs/sort-pipe/src/lib/sort.pipe.ts",
"line": 1
}
]
}CI integration
# Fail the pipeline if any dead exports are found
ngx-phantom analyze --exclude-tests --failprune — remove dead exports from barrel files
ngx-phantom prune [path]Runs the same analysis as analyze, then rewrites each library's barrel file (index.ts / public-api.ts) to remove export statements whose symbols have zero consumers.
- Named exports (
export { Foo } from './foo') are removed or trimmed safely. - Multi-line exports are handled correctly.
- Wildcard exports (
export * from './foo') are left untouched and reported separately.
Always preview first with --dry-run:
ngx-phantom prune --dry-runngx-phantom ▸ Prune
──────────────────────────────────────────────────
DRY RUN — no files will be modified
Discovering workspace... found 24 libraries
Analyzing...
Found 12 dead export(s). Pruning barrel files...
Would prune 1 export(s) from @myorg/sort-pipe
Would prune 1 export(s) from @myorg/button
...
Done in 0.31s — would remove 11 export(s)Apply the changes:
ngx-phantom pruneFlags
| Flag | Default | Description |
|---|---|---|
| --root | . | Workspace root |
| --dry-run | false | Preview changes without writing files |
| --exclude-tests | false | Exclude *.spec.ts files from consumer analysis |
| --workers | 2×CPU | Number of parallel file-scan workers |
explain — understand why a symbol is dead
ngx-phantom explain --lib <library> --symbol <symbol>Shows a detailed breakdown for a single exported symbol: which files import from the library, which symbols they consume, and whether the specific symbol has any consumers. Use this to verify a reported dead export before pruning.
ngx-phantom explain --lib @myorg/sort-pipe --symbol SortPipengx-phantom ▸ Explain
──────────────────────────────────────────────────
Library : @myorg/sort-pipe
Symbol : SortPipe
Export found: SortPipe
Defined at : libs/sort-pipe/src/lib/sort.pipe.ts:1
@myorg/sort-pipe is imported by 4 file(s):
apps/my-app/src/app/app.module.ts
apps/other-app/src/app/app.module.ts
...
Symbols consumed from @myorg/sort-pipe (1):
SortPipeModule
✗ "SortPipe" is exported but NEVER imported by any consumer.
Similar symbols that ARE consumed: SortPipeModuleFlags
| Flag | Default | Description |
|---|---|---|
| --root | . | Workspace root |
| --lib | required | Library name, e.g. @myorg/ui |
| --symbol | required | Symbol name, e.g. ButtonComponent |
| --exclude-tests | false | Exclude *.spec.ts files |
| --workers | 2×CPU | Number of parallel file-scan workers |
How it works
Workspace discovery — reads
tsconfig.base.jsonand extracts allpathsentries. Paths pointing todist/directories are resolved to the sourcesrc/index.tsorsrc/public-api.ts.Export parsing — for each library entry point, parses all
exportstatements:export { Foo, Bar } from './path'export type { Foo } from './path'export * from './path'— recursively resolvedexport * as Namespace from './path'- Direct declarations (
export class Foo,export const foo, etc.) when resolving wildcards
Import scanning — walks every
.tsfile in the workspace (excludingnode_modules,dist,.git,.angular,coverage) and records all import statements referencing known library paths:- Named imports:
import { Foo } from '@myorg/ui' - Type imports:
import type { Foo } from '@myorg/ui' - Namespace imports:
import * as X from '@myorg/ui'(marks entire lib as opaque) - Dynamic imports:
import('@myorg/ui') - Subpath imports:
import { Foo } from '@myorg/ui/testing'(mapped to@myorg/ui) - Multi-line imports are handled correctly
- Named imports:
Dead export analysis — symbols exported by a library but absent from any consumer's import set are reported as dead. Libraries with namespace imports (
import * as X) are skipped entirely since consumption cannot be determined statically.Pruning — rewrites barrel files in-place using regex replacement that handles single-line and multi-line
export { ... }blocks. Wildcard exports are intentionally left untouched.
Common patterns explained
NgModule wrapper pattern
A common source of confusion: many Angular libraries export both the directive/pipe class and an NgModule that declares it:
// index.ts
export { SortPipe } from './lib/sort.pipe';
export { SortPipeModule } from './lib/sort-pipe.module';If all consumers import SortPipeModule but nobody imports SortPipe directly, ngx-phantom will correctly flag SortPipe as a dead export. The class is used at runtime via the module declaration, but it is not part of the public TypeScript API.
Use explain to verify:
ngx-phantom explain --lib @myorg/sort-pipe --symbol SortPipe
# Shows: 4 files import from @myorg/sort-pipe, all consume SortPipeModuleIf you are migrating to standalone components you can safely prune the class export alongside removing the module.
Same symbol exported from multiple libraries
If SymbolA is exported from both @myorg/lib-a and @myorg/lib-b, but consumers only import it from @myorg/lib-b, then SymbolA from @myorg/lib-a will be flagged as dead. This is correct — the public API of @myorg/lib-a contains a redundant re-export.
Namespace imports
Libraries consumed via import * as X from '@myorg/lib' cannot be analyzed statically. ngx-phantom skips them entirely and lists them under "Skipped libraries" in the report.
Limitations
- Namespace imports (
import * as X from '@lib') prevent per-symbol analysis for that library. - Wildcard exports (
export * from './path') are not modified byprune— reported separately for manual review. - Template-only usage — if a component or directive is used only in an Angular template (not imported in TypeScript), it will still appear in the import statement of the component's
.tsfile, so this is not an issue in practice. - Dynamic string imports —
import('@myorg/' + name)cannot be resolved statically and will be missed. - Type-only consumers — symbols imported only as TypeScript types are counted as consumers (by design — removing a type export is still a breaking API change).
History & Go → Zig rewrite
ngx-phantom was originally written in Go. It has since been fully rewritten in Zig for significantly lower resource usage and a smaller distribution footprint.
Benchmarked on a real Angular monorepo (~150 libraries, ~4,000 TypeScript files):
| Metric | Go | Zig | Improvement | |---|---|---|---| | Binary size | 5.9 MB | 288 KB | 95% smaller | | Wall-clock time | 271.9 ms | 226.3 ms | ~17% faster | | CPU time (user) | 1,017 ms | 63.7 ms | 16× less CPU | | Source lines | ~2,000 | 1,931 | comparable |
