pruny 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -44
- package/dist/index.js +24 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# pruny
|
|
2
2
|
|
|
3
|
-
Find and remove unused Next.js
|
|
3
|
+
Find and remove unused code in Next.js and NestJS projects.
|
|
4
|
+
|
|
5
|
+
Pruny scans your codebase using regex-based static analysis to detect unused API routes, page links, exports, files, public assets, and NestJS service methods. Works with monorepos out of the box.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
@@ -10,67 +12,153 @@ npm install -g pruny
|
|
|
10
12
|
npx pruny
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## What It Detects
|
|
16
|
+
|
|
17
|
+
| Scanner | What it finds |
|
|
18
|
+
| :------ | :------------ |
|
|
19
|
+
| **API Routes** | Unused Next.js `route.ts` handlers and NestJS controller methods |
|
|
20
|
+
| **Broken Links** | `<Link>`, `router.push()`, `redirect()` pointing to pages that don't exist |
|
|
21
|
+
| **Unused Exports** | Named exports and class methods not imported anywhere |
|
|
22
|
+
| **Unused Files** | Source files not reachable from any entry point |
|
|
23
|
+
| **Unused Services** | NestJS service methods never called by controllers or other services |
|
|
24
|
+
| **Public Assets** | Images/files in `public/` not referenced in code |
|
|
25
|
+
| **Source Assets** | Media files in `src/` not referenced in code |
|
|
26
|
+
| **Missing Assets** | References to files in `public/` that don't exist |
|
|
14
27
|
|
|
15
28
|
## CLI Commands
|
|
16
29
|
|
|
17
|
-
| Command
|
|
18
|
-
|
|
|
19
|
-
| `pruny`
|
|
20
|
-
| `pruny --
|
|
21
|
-
| `pruny --
|
|
22
|
-
| `pruny --
|
|
23
|
-
| `pruny --
|
|
24
|
-
| `pruny --
|
|
25
|
-
| `pruny --
|
|
26
|
-
| `pruny --
|
|
27
|
-
| `pruny --
|
|
28
|
-
| `pruny --
|
|
29
|
-
| `pruny
|
|
30
|
-
| `pruny
|
|
30
|
+
| Command | Description |
|
|
31
|
+
| :------ | :---------- |
|
|
32
|
+
| `pruny` | Interactive scan (auto-detects monorepo apps) |
|
|
33
|
+
| `pruny --all` | CI mode: scan all apps, exit 1 if issues found |
|
|
34
|
+
| `pruny --fix` | Interactively delete unused items |
|
|
35
|
+
| `pruny --dry-run` | Simulate fix mode and output a JSON report |
|
|
36
|
+
| `pruny --app <name>` | Scan a specific app in a monorepo |
|
|
37
|
+
| `pruny --folder <path>` | Scan a specific folder for routes/controllers |
|
|
38
|
+
| `pruny --cleanup <items>` | Quick cleanup: `routes`, `exports`, `public`, `files` |
|
|
39
|
+
| `pruny --filter <pattern>` | Filter results by path or app name |
|
|
40
|
+
| `pruny --ignore-apps <list>` | Skip specific apps (comma-separated) |
|
|
41
|
+
| `pruny --no-public` | Skip public asset scanning |
|
|
42
|
+
| `pruny --json` | Output results as JSON |
|
|
43
|
+
| `pruny -v, --verbose` | Verbose debug logging |
|
|
44
|
+
| `pruny --dir <path>` | Set target directory (default: `./`) |
|
|
45
|
+
| `pruny -c, --config <path>` | Path to config file |
|
|
46
|
+
| `pruny init` | Generate a `pruny.config.json` with defaults |
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
Create `pruny.config.json` in your project root (or run `pruny init`):
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"ignore": {
|
|
55
|
+
"routes": ["/api/webhooks/**", "/api/cron/**"],
|
|
56
|
+
"folders": ["node_modules", ".next", "dist"],
|
|
57
|
+
"files": ["*.test.ts", "*.spec.ts"],
|
|
58
|
+
"links": ["/custom-path", "/legacy/*"]
|
|
59
|
+
},
|
|
60
|
+
"extensions": [".ts", ".tsx", ".js", ".jsx"]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Ignore Options
|
|
65
|
+
|
|
66
|
+
| Key | What it does | Example |
|
|
67
|
+
| :-- | :----------- | :------ |
|
|
68
|
+
| `ignore.routes` | Skip API routes matching these patterns | `["/api/webhooks/**"]` |
|
|
69
|
+
| `ignore.folders` | Exclude directories from scanning | `["node_modules", "dist"]` |
|
|
70
|
+
| `ignore.files` | Exclude specific files from scanning | `["*.test.ts"]` |
|
|
71
|
+
| `ignore.links` | Suppress broken link warnings for these paths | `["/view_seat", "/admin/*"]` |
|
|
72
|
+
|
|
73
|
+
All patterns support glob syntax (`*` matches any characters, `**` matches nested paths).
|
|
74
|
+
|
|
75
|
+
Pruny also reads `.gitignore` and automatically excludes those folders.
|
|
76
|
+
|
|
77
|
+
### Additional Config Options
|
|
78
|
+
|
|
79
|
+
| Key | What it does |
|
|
80
|
+
| :-- | :----------- |
|
|
81
|
+
| `nestGlobalPrefix` | NestJS global route prefix (e.g., `"api/v1"`) |
|
|
82
|
+
| `extraRoutePatterns` | Additional glob patterns to detect route files |
|
|
83
|
+
| `excludePublic` | Set `true` to skip public asset scanning |
|
|
31
84
|
|
|
32
|
-
###
|
|
85
|
+
### Config File Locations
|
|
33
86
|
|
|
34
|
-
Pruny
|
|
87
|
+
Pruny searches for config files recursively across your project:
|
|
88
|
+
- `pruny.config.json`
|
|
89
|
+
- `.prunyrc.json`
|
|
90
|
+
- `.prunyrc`
|
|
35
91
|
|
|
36
|
-
|
|
37
|
-
- **Disable it**: Use `--no-public` flag.
|
|
38
|
-
```bash
|
|
39
|
-
pruny --no-public
|
|
40
|
-
```
|
|
41
|
-
- **How it works**: It checks if filenames in `public/` (e.g., `logo.png` or `/images/logo.png`) are referenced in your code.
|
|
92
|
+
In monorepos, configs from multiple apps are merged together. CLI `--config` takes precedence.
|
|
42
93
|
|
|
43
|
-
##
|
|
94
|
+
## Multi-Tenant / Subdomain Routing
|
|
44
95
|
|
|
45
|
-
|
|
96
|
+
Pruny automatically handles multi-tenant architectures where routes live under dynamic segments like `[domain]`.
|
|
97
|
+
|
|
98
|
+
For example, if your file structure is:
|
|
99
|
+
```
|
|
100
|
+
app/(code)/tenant_sites/[domain]/view_seat/page.tsx
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
And your components reference `/view_seat` (resolved at runtime via subdomain), Pruny recognizes this as a valid route and will **not** report it as a broken link.
|
|
104
|
+
|
|
105
|
+
If auto-detection doesn't cover your case, use `ignore.links` in config:
|
|
46
106
|
|
|
47
107
|
```json
|
|
48
108
|
{
|
|
49
|
-
"dir": "./",
|
|
50
109
|
"ignore": {
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"files": ["*.test.ts", "*.spec.ts"]
|
|
54
|
-
},
|
|
55
|
-
"extensions": [".ts", ".tsx", ".js", ".jsx"]
|
|
110
|
+
"links": ["/view_seat", "/review", "/custom-path"]
|
|
111
|
+
}
|
|
56
112
|
}
|
|
57
113
|
```
|
|
58
114
|
|
|
59
|
-
##
|
|
115
|
+
## Monorepo Support
|
|
116
|
+
|
|
117
|
+
Pruny auto-detects monorepos by looking for an `apps/` directory. It scans each app independently but checks references across the entire monorepo root.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Scan all apps (CI-friendly, exits 1 on issues)
|
|
121
|
+
pruny --all
|
|
122
|
+
|
|
123
|
+
# Scan a specific app
|
|
124
|
+
pruny --app web
|
|
125
|
+
|
|
126
|
+
# Skip certain apps
|
|
127
|
+
pruny --ignore-apps admin,docs
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## CI Integration
|
|
60
131
|
|
|
61
|
-
|
|
62
|
-
- 🗑️ `--fix` flag to delete unused routes
|
|
63
|
-
- ⚡ Auto-detects `vercel.json` cron routes
|
|
64
|
-
- 📁 Default ignores: `node_modules`, `.next`, `dist`, `.git`
|
|
65
|
-
- 🎨 Beautiful CLI output
|
|
132
|
+
Add to your CI pipeline to catch unused code before merging:
|
|
66
133
|
|
|
67
|
-
|
|
134
|
+
```bash
|
|
135
|
+
npx pruny --all
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This scans all monorepo apps and exits with code 1 if any issues are found. Combine with `--json` for machine-readable output.
|
|
139
|
+
|
|
140
|
+
## How It Works
|
|
141
|
+
|
|
142
|
+
1. **Route Detection**: Finds all `app/api/**/route.ts` (Next.js) and `*.controller.ts` (NestJS) files
|
|
143
|
+
2. **Link Detection**: Finds `<Link>`, `router.push()`, `redirect()`, `href:` patterns and validates against known page routes
|
|
144
|
+
3. **Reference Scanning**: Searches the entire codebase for string references to routes, exports, and assets
|
|
145
|
+
4. **Dynamic Route Matching**: Understands `[id]`, `[...slug]`, `[[...slug]]` dynamic segments
|
|
146
|
+
5. **Fix Mode**: Removes unused methods, exports, and files with a cascading second pass to catch newly dead code
|
|
147
|
+
|
|
148
|
+
### Vercel Cron Detection
|
|
149
|
+
|
|
150
|
+
Routes listed in `vercel.json` cron jobs are automatically marked as used:
|
|
151
|
+
```json
|
|
152
|
+
{ "crons": [{ "path": "/api/cron/cleanup", "schedule": "0 0 * * *" }] }
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Debug Mode
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
DEBUG_PRUNY=1 pruny
|
|
159
|
+
```
|
|
68
160
|
|
|
69
|
-
|
|
70
|
-
2. **Nest.js**: Finds all `*.controller.ts` files and extracts mapped routes (e.g., `@Get('users')`).
|
|
71
|
-
3. Scans codebase for client-side usages (e.g., `fetch`, `axios`, or string literals matching the route).
|
|
72
|
-
4. Reports routes with no detected references.
|
|
73
|
-
5. `--fix` deletes the unused route file or method.
|
|
161
|
+
Enables verbose logging across all scanners.
|
|
74
162
|
|
|
75
163
|
## License
|
|
76
164
|
|
package/dist/index.js
CHANGED
|
@@ -15791,9 +15791,21 @@ function matchesRoute(refPath, routes, routeSegments) {
|
|
|
15791
15791
|
for (const routeSeg of routeSegments) {
|
|
15792
15792
|
if (matchSegments(refSegments, routeSeg))
|
|
15793
15793
|
return true;
|
|
15794
|
+
if (matchesDynamicSuffix(refSegments, routeSeg))
|
|
15795
|
+
return true;
|
|
15794
15796
|
}
|
|
15795
15797
|
return false;
|
|
15796
15798
|
}
|
|
15799
|
+
function matchesDynamicSuffix(refSegments, routeSegments) {
|
|
15800
|
+
if (refSegments.length >= routeSegments.length)
|
|
15801
|
+
return false;
|
|
15802
|
+
const prefixLen = routeSegments.length - refSegments.length;
|
|
15803
|
+
const prefix = routeSegments.slice(0, prefixLen);
|
|
15804
|
+
if (!prefix.some((s) => /^\[.+\]$/.test(s)))
|
|
15805
|
+
return false;
|
|
15806
|
+
const tail = routeSegments.slice(prefixLen);
|
|
15807
|
+
return matchSegments(refSegments, tail);
|
|
15808
|
+
}
|
|
15797
15809
|
function matchSegments(refSegments, routeSegments) {
|
|
15798
15810
|
let ri = 0;
|
|
15799
15811
|
let si = 0;
|
|
@@ -15868,7 +15880,11 @@ async function scanBrokenLinks(config) {
|
|
|
15868
15880
|
if (!cleaned || cleaned === "/")
|
|
15869
15881
|
continue;
|
|
15870
15882
|
if (!matchesRoute(cleaned, knownRoutes, routeSegmentsList)) {
|
|
15871
|
-
const
|
|
15883
|
+
const ignorePatterns = [
|
|
15884
|
+
...config.ignore.links || [],
|
|
15885
|
+
...config.ignore.routes
|
|
15886
|
+
];
|
|
15887
|
+
const isIgnored = ignorePatterns.some((ignorePath) => {
|
|
15872
15888
|
const pattern2 = ignorePath.replace(/\*/g, ".*");
|
|
15873
15889
|
return new RegExp(`^${pattern2}$`).test(cleaned);
|
|
15874
15890
|
});
|
|
@@ -16263,7 +16279,8 @@ var DEFAULT_CONFIG = {
|
|
|
16263
16279
|
"**/.git/**",
|
|
16264
16280
|
"**/coverage/**"
|
|
16265
16281
|
],
|
|
16266
|
-
files: []
|
|
16282
|
+
files: [],
|
|
16283
|
+
links: []
|
|
16267
16284
|
},
|
|
16268
16285
|
extensions: [".ts", ".tsx", ".js", ".jsx"],
|
|
16269
16286
|
nestGlobalPrefix: "",
|
|
@@ -16289,7 +16306,8 @@ function loadConfig(options) {
|
|
|
16289
16306
|
const mergedIgnore = {
|
|
16290
16307
|
routes: [...DEFAULT_CONFIG.ignore.routes || []],
|
|
16291
16308
|
folders: [...DEFAULT_CONFIG.ignore.folders || []],
|
|
16292
|
-
files: [...DEFAULT_CONFIG.ignore.files || []]
|
|
16309
|
+
files: [...DEFAULT_CONFIG.ignore.files || []],
|
|
16310
|
+
links: [...DEFAULT_CONFIG.ignore.links || []]
|
|
16293
16311
|
};
|
|
16294
16312
|
let mergedExtensions = [...DEFAULT_CONFIG.extensions];
|
|
16295
16313
|
let nestGlobalPrefix = DEFAULT_CONFIG.nestGlobalPrefix;
|
|
@@ -16313,6 +16331,8 @@ function loadConfig(options) {
|
|
|
16313
16331
|
mergedIgnore.folders.push(...config.ignore.folders.map(prefixPattern));
|
|
16314
16332
|
if (config.ignore?.files)
|
|
16315
16333
|
mergedIgnore.files.push(...config.ignore.files.map(prefixPattern));
|
|
16334
|
+
if (config.ignore?.links)
|
|
16335
|
+
mergedIgnore.links.push(...config.ignore.links);
|
|
16316
16336
|
if (config.extensions)
|
|
16317
16337
|
mergedExtensions = [...new Set([...mergedExtensions, ...config.extensions])];
|
|
16318
16338
|
if (config.nestGlobalPrefix)
|
|
@@ -16330,6 +16350,7 @@ function loadConfig(options) {
|
|
|
16330
16350
|
mergedIgnore.routes = [...new Set(mergedIgnore.routes)];
|
|
16331
16351
|
mergedIgnore.folders = [...new Set(mergedIgnore.folders)];
|
|
16332
16352
|
mergedIgnore.files = [...new Set(mergedIgnore.files)];
|
|
16353
|
+
mergedIgnore.links = [...new Set(mergedIgnore.links)];
|
|
16333
16354
|
return {
|
|
16334
16355
|
dir: cwd,
|
|
16335
16356
|
ignore: mergedIgnore,
|