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.
Files changed (3) hide show
  1. package/README.md +132 -44
  2. package/dist/index.js +24 -3
  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 API routes & Nest.js Controllers. 🪓
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
- ## Usage
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 | Description |
18
- | :--------------------------- | :--------------------------------------------------------------------------------------- |
19
- | `pruny` | Scan for unused items interactively (monorepo-aware). |
20
- | `pruny --dir <path>` | Set the target project directory (default: `./`). |
21
- | `pruny --app <name>` | Scan a specific application within a monorepo. |
22
- | `pruny --folder <path>` | Scan a specific folder OR sub-directory for routes/controllers. |
23
- | `pruny --fix` | Automatically delete unused items found during scan. |
24
- | `pruny --cleanup <items>` | Quick cleanup: `routes`, `exports`, `public`, `files`. (e.g. `--cleanup routes,exports`) |
25
- | `pruny --filter <pattern>` | Filter results by string (app name, file path, etc). |
26
- | `pruny --ignore-apps <list>` | Comma-separated list of apps to skip in monorepos. |
27
- | `pruny --no-public` | Disable scanning of public assets. |
28
- | `pruny --json` | Output scan results as JSON for automation. |
29
- | `pruny -v, --verbose` | Show detailed debug logging and trace info. |
30
- | `pruny init` | Create a `pruny.config.json` configuration file. |
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
- ### Public Asset Scanning (New in v1.1.0)
85
+ ### Config File Locations
33
86
 
34
- Pruny automatically scans your `public/` directory for unused images and files.
87
+ Pruny searches for config files recursively across your project:
88
+ - `pruny.config.json`
89
+ - `.prunyrc.json`
90
+ - `.prunyrc`
35
91
 
36
- - **Enabled by default**: Run `npx pruny` and it will show unused assets, excluding ignored folders.
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
- ## Config
94
+ ## Multi-Tenant / Subdomain Routing
44
95
 
45
- Create `pruny.config.json` (optional):
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
- "routes": ["/api/webhooks/**", "/api/cron/**"],
52
- "folders": ["node_modules", ".next", "dist"],
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
- ## Features
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
- - 🔍 Detects unused Next.js API routes & Nest.js Controller methods
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
- ## How it works
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
- 1. **Next.js**: Finds all `app/api/**/route.ts` files.
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 isIgnored = config.ignore.routes.some((ignorePath) => {
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.37.0",
3
+ "version": "1.38.0",
4
4
  "description": "Find and remove unused Next.js API routes & Nest.js Controllers",
5
5
  "type": "module",
6
6
  "files": [