Package Exports
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ngx-phantom) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
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 and source files
ngx-phantom prune [path]Runs the same analysis as analyze, then performs a two-phase rewrite:
Phase 1 — barrel files (index.ts / public-api.ts)
Removes or trims export { Foo } from './foo' statements whose symbols have zero consumers.
- Named exports are removed or trimmed safely (multi-specifier lines are handled correctly).
- Wildcard exports (
export * from './foo') are left untouched and reported separately.
Phase 2 — source files (components, services, models, …)
For each dead export found in a non-barrel source file:
| Situation | Action |
|---|---|
| Symbol is referenced elsewhere in the same file | Strip the export keyword only — the declaration stays private |
| Symbol is not used anywhere in the file | Remove the entire declaration block, including any preceding decorator lines (@Component, @Injectable, …) |
| No exports remain after the above (empty file, imports-only file, or all exports stripped) | Delete the file |
This means a single prune run can clean up barrel entries, de-publicise declarations, and delete source files that have become dead islands — all in one pass.
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 — two-phase rewrite:
- Phase 1 (barrel files): rewrites each library's
index.ts/public-api.tsin-place, removing or trimmingexport { ... } from '...'specifiers for dead symbols. Wildcard exports are intentionally left untouched. - Phase 2 (source files): for each dead export whose declaration lives in a non-barrel source file, the declaration is either de-publicised (strip
export) or fully removed (the entire block including decorators). If noexportstatement remains in the file after this pass — because all exports were dead, or the file was already empty / import-only — the file is deleted.
- Phase 1 (barrel files): rewrites each library's
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).
- Source-file pruning is heuristic — declaration end detection uses brace-balancing and semicolon scanning. Multi-line type aliases without a trailing semicolon, or unusual formatting, may not be handled perfectly; always review changes with
--dry-runand your version control diff before committing.
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 |