tsc-rs analyze
tsc-rs analyze runs fast, parser-level static analysis over a TypeScript monorepo. It does not type-check. It parses TypeScript and JavaScript files, resolves their imports into an import graph, and runs audits against that graph.
analyze externals#
Determines which entries of a Next.js serverExternalPackages list are actually reachable from your source code.
| Category | Meaning | Action |
|---|---|---|
| Unreachable | No file in the import graph references the package | Safe to remove |
| Type-only | Referenced only through import type |
Safe to remove |
| Runtime | Referenced with runtime imports | Keep |
Usage#
tsc-rs analyze externals --entry <dir> --externals <file> [-p <tsconfig.json>] [--json]
| Flag | Meaning |
|---|---|
--entry <dir> |
Source directory to scan. .ts and .tsx files are discovered recursively |
--externals <file> |
File that contains the externals list |
-p <tsconfig.json> |
Load path aliases from a tsconfig. Optional |
--json |
Print JSON instead of the human-readable report |
The --externals file can be a JSON array (["pino", "sharp"]), a next.config.mjs (the serverExternalPackages array is extracted), or plain text with one package name per line.
Example#
tsc-rs analyze externals \
--entry apps/app/src \
-p apps/app/tsconfig.json \
--externals apps/app/next.config.mjs
Externals Audit: 46 runtime, 8 type-only, 57 unreachable. 65 removable out of 111
Scanned 7856 files, found 285 unique packages
=== UNREACHABLE (57): safe to remove ===
@auth/prisma-adapter
@babel/runtime
...
=== TYPE-ONLY (8): safe to remove (no runtime effect) ===
@acme/services-calendar (1 imports)
...
=== RUNTIME (46): keep ===
@aws-sdk/client-s3 (48 imports)
...
The exact punctuation of the report may differ; the structure is as shown.
analyze dep-graph#
Shows which files have the most imports, which reveals architectural bottlenecks, for example every route pulling in an entire generated database client.
Usage#
tsc-rs analyze dep-graph --entry <dir> [-p <tsconfig.json>] [--top <N>]
--top sets how many files to show, 20 by default.
Example#
tsc-rs analyze dep-graph --entry apps/app/src/server/api/routers --top 10
Top 10 files by import fan-out:
Count File
1193 packages/db/dist/prisma-client/models.d.ts
11 packages/shared-types/dist/index.d.ts
9 apps/app/src/server/api/routers/__tests__/generated/shop.test.ts
...
Total: 1319 files, 4784 edges, 22 unique packages
How it works#
- File discovery. Finds
.tsand.tsxfiles recursively, skippingnode_modules,.next,dist,.git,.turboand.bext. - Parsing. Uses the tsc-rs parser. No type checking is needed.
- Specifier extraction. Walks the whole AST, including function bodies, class methods and
tryblocks, and collects static imports, re-exports, dynamicimport()calls andrequire()calls. It records whether each import is type-only. - Resolution. Uses the tsc-rs Node module resolver, with path aliases from the tsconfig.
- Classification. Relative imports are followed into the file. Path-aliased imports resolve to local files and are not counted as packages. Node built-ins are filtered out. External packages are recorded with the import chain that reaches them.
Performance#
On a real monorepo, 5,700 entry files (more than 7,800 files walked once resolved imports are followed, 285 unique packages) take about 3.5 seconds. There is no type checking, no bundler and no Node.js process involved.
Reading the results#
- Unreachable. Cross-check against dynamic loaders that the parser cannot trace. A package loaded through an opaque dynamic import is invisible to static analysis.
- Type-only. Safe to remove unless the package is also loaded dynamically at run time.
- Runtime. Keep. The import count helps you prioritize: hundreds of imports means core infrastructure, one import may be a rarely used path.
Transitive dependencies of an externalized package do not need to be listed. Only packages imported directly from your source do.
Limitations#
- Dynamic specifiers.
require(variable)andimport()with a non-literal argument cannot be resolved. - Conditional requires.
if (condition) require("pkg")is detected, but the condition is not evaluated: the package always counts as used. - node_modules boundary. The walker stops at
node_modules, so dependencies inside packages are not traced. - Self-referencing tsconfig. Handled, but
extendschains are not followed in that case.
Found a mistake on this page? Open an issue on GitHub.