pruny 1.0.6 ā 1.0.7
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 +11 -0
- package/docs/index.md +5 -1
- package/llms.txt +5 -1
- package/package.json +1 -1
- package/src/config.ts +14 -3
- package/src/index.ts +54 -30
- package/src/scanner.ts +8 -0
- package/src/scanners/public-assets.ts +89 -0
- package/src/types.ts +12 -0
package/README.md
CHANGED
|
@@ -29,6 +29,17 @@ pruny --json
|
|
|
29
29
|
pruny -v
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
### Public Asset Scanning (New in v1.1.0)
|
|
33
|
+
|
|
34
|
+
Pruny automatically scans your `public/` directory for unused images and files.
|
|
35
|
+
|
|
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.
|
|
42
|
+
|
|
32
43
|
## Config
|
|
33
44
|
|
|
34
45
|
Create `pruny.config.json` (optional):
|
package/docs/index.md
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
> Find and remove unused Next.js API routes.
|
|
4
4
|
|
|
5
5
|
## Summary
|
|
6
|
-
Pruny is a CLI tool that scans Next.js projects (App Router) to identify
|
|
6
|
+
Pruny is a CLI tool that scans Next.js projects (App Router) to identify:
|
|
7
|
+
1. **Unused API Routes**: Endpoints not referenced in the codebase.
|
|
8
|
+
2. **Unused Public Assets**: Images and files in `public/` that are never used.
|
|
9
|
+
|
|
10
|
+
It supports auto-detection of usage via `fetch`, `axios`, `useSWR`, and respects `vercel.json` cron jobs.
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
package/llms.txt
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
> Find and remove unused Next.js API routes.
|
|
4
4
|
|
|
5
5
|
## Summary
|
|
6
|
-
Pruny is a CLI tool that scans Next.js projects (App Router) to identify
|
|
6
|
+
Pruny is a CLI tool that scans Next.js projects (App Router) to identify:
|
|
7
|
+
1. **Unused API Routes**: Endpoints not referenced in the codebase.
|
|
8
|
+
2. **Unused Public Assets**: Images and files in `public/` that are never used.
|
|
9
|
+
|
|
10
|
+
It supports auto-detection of usage via `fetch`, `axios`, `useSWR`, and respects `vercel.json` cron jobs.
|
|
7
11
|
|
|
8
12
|
## Installation
|
|
9
13
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -15,6 +15,7 @@ const DEFAULT_CONFIG: Config = {
|
|
|
15
15
|
interface CLIOptions {
|
|
16
16
|
dir?: string;
|
|
17
17
|
config?: string;
|
|
18
|
+
excludePublic?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -38,11 +39,21 @@ export function loadConfig(options: CLIOptions): Config {
|
|
|
38
39
|
return {
|
|
39
40
|
dir: options.dir || fileConfig.dir || DEFAULT_CONFIG.dir,
|
|
40
41
|
ignore: {
|
|
41
|
-
routes:
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
routes: [
|
|
43
|
+
...(DEFAULT_CONFIG.ignore.routes || []),
|
|
44
|
+
...(fileConfig.ignore?.routes || []),
|
|
45
|
+
],
|
|
46
|
+
folders: [
|
|
47
|
+
...(DEFAULT_CONFIG.ignore.folders || []),
|
|
48
|
+
...(fileConfig.ignore?.folders || []),
|
|
49
|
+
],
|
|
50
|
+
files: [
|
|
51
|
+
...(DEFAULT_CONFIG.ignore.files || []),
|
|
52
|
+
...(fileConfig.ignore?.files || []),
|
|
53
|
+
],
|
|
44
54
|
},
|
|
45
55
|
extensions: fileConfig.extensions || DEFAULT_CONFIG.extensions,
|
|
56
|
+
excludePublic: options.excludePublic ?? fileConfig.excludePublic ?? false,
|
|
46
57
|
};
|
|
47
58
|
}
|
|
48
59
|
|
package/src/index.ts
CHANGED
|
@@ -17,11 +17,13 @@ program
|
|
|
17
17
|
.option('--fix', 'Delete unused API routes')
|
|
18
18
|
.option('-c, --config <path>', 'Path to config file')
|
|
19
19
|
.option('--json', 'Output as JSON')
|
|
20
|
+
.option('--no-public', 'Disable public assets scanning')
|
|
20
21
|
.option('-v, --verbose', 'Show detailed info')
|
|
21
22
|
.action(async (options) => {
|
|
22
23
|
const config = loadConfig({
|
|
23
24
|
dir: options.dir,
|
|
24
25
|
config: options.config,
|
|
26
|
+
excludePublic: !options.public, // commander handles --no-public as public: false
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
// Resolve absolute path
|
|
@@ -51,27 +53,45 @@ program
|
|
|
51
53
|
console.log(` Total routes: ${result.total}`);
|
|
52
54
|
console.log(chalk.green(` Used routes: ${result.used}`));
|
|
53
55
|
console.log(chalk.red(` Unused routes: ${result.unused}`));
|
|
56
|
+
|
|
57
|
+
if (result.publicAssets) {
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log(chalk.bold('š¼ļø Public Assets'));
|
|
60
|
+
console.log(` Total assets: ${result.publicAssets.total}`);
|
|
61
|
+
console.log(chalk.green(` Used assets: ${result.publicAssets.used}`));
|
|
62
|
+
console.log(chalk.red(` Unused assets: ${result.publicAssets.unused}`));
|
|
63
|
+
}
|
|
54
64
|
console.log('');
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
// 1. API Routes Logic
|
|
67
|
+
const unusedRoutes = result.routes.filter((r) => !r.used);
|
|
68
|
+
if (unusedRoutes.length > 0) {
|
|
69
|
+
console.log(chalk.red.bold('ā Unused API Routes:\n'));
|
|
70
|
+
for (const route of unusedRoutes) {
|
|
71
|
+
console.log(chalk.red(` ${route.path}`));
|
|
72
|
+
console.log(chalk.dim(` ā ${route.filePath}`));
|
|
73
|
+
}
|
|
74
|
+
console.log('');
|
|
75
|
+
} else if (result.total === 0) {
|
|
57
76
|
console.log(chalk.yellow('ā ļø No API routes found.\n'));
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const unused = result.routes.filter((r) => !r.used);
|
|
62
|
-
|
|
63
|
-
if (unused.length === 0) {
|
|
77
|
+
} else {
|
|
64
78
|
console.log(chalk.green('ā
All API routes are used!\n'));
|
|
65
|
-
return;
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
// 2. Public Assets Logic
|
|
82
|
+
if (result.publicAssets) {
|
|
83
|
+
const unusedAssets = result.publicAssets.assets.filter(a => !a.used);
|
|
84
|
+
if (unusedAssets.length > 0) {
|
|
85
|
+
console.log(chalk.red.bold('ā Unused Public Assets:\n'));
|
|
86
|
+
for (const asset of unusedAssets) {
|
|
87
|
+
console.log(chalk.red(` ${asset.relativePath}`));
|
|
88
|
+
console.log(chalk.dim(` ā ${asset.path}`));
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
} else if (result.publicAssets.total > 0) {
|
|
92
|
+
console.log(chalk.green('ā
All public assets are used!\n'));
|
|
93
|
+
}
|
|
73
94
|
}
|
|
74
|
-
console.log('');
|
|
75
95
|
|
|
76
96
|
// Show used routes in verbose mode
|
|
77
97
|
if (options.verbose) {
|
|
@@ -97,24 +117,28 @@ program
|
|
|
97
117
|
|
|
98
118
|
// --fix: Delete unused routes
|
|
99
119
|
if (options.fix) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
120
|
+
if (unusedRoutes.length > 0) {
|
|
121
|
+
console.log(chalk.yellow.bold('šļø Deleting unused routes...\n'));
|
|
122
|
+
|
|
123
|
+
for (const route of unusedRoutes) {
|
|
124
|
+
const routeDir = dirname(join(config.dir, route.filePath));
|
|
125
|
+
try {
|
|
126
|
+
rmSync(routeDir, { recursive: true, force: true });
|
|
127
|
+
console.log(chalk.red(` Deleted: ${route.filePath}`));
|
|
128
|
+
} catch (err) {
|
|
129
|
+
console.log(
|
|
130
|
+
chalk.yellow(` Failed to delete: ${route.filePath}`)
|
|
131
|
+
);
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
|
-
}
|
|
113
134
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.green(`\nā
Deleted ${unusedRoutes.length} unused route(s).\n`)
|
|
137
|
+
);
|
|
138
|
+
} else {
|
|
139
|
+
console.log(chalk.yellow('No unused routes to delete.\n'));
|
|
140
|
+
}
|
|
141
|
+
} else if (unusedRoutes.length > 0) {
|
|
118
142
|
console.log(
|
|
119
143
|
chalk.dim('š” Run with --fix to delete unused routes.\n')
|
|
120
144
|
);
|
package/src/scanner.ts
CHANGED
|
@@ -179,10 +179,18 @@ export async function scan(config: Config): Promise<ScanResult> {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// 7. Scan public assets (if not excluded)
|
|
183
|
+
let publicAssets;
|
|
184
|
+
if (!config.excludePublic) {
|
|
185
|
+
const { scanPublicAssets } = await import('./scanners/public-assets.js');
|
|
186
|
+
publicAssets = await scanPublicAssets(config);
|
|
187
|
+
}
|
|
188
|
+
|
|
182
189
|
return {
|
|
183
190
|
total: routes.length,
|
|
184
191
|
used: routes.filter((r) => r.used).length,
|
|
185
192
|
unused: routes.filter((r) => !r.used).length,
|
|
186
193
|
routes,
|
|
194
|
+
publicAssets,
|
|
187
195
|
};
|
|
188
196
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import type { Config } from '../types.js';
|
|
5
|
+
|
|
6
|
+
export interface PublicAsset {
|
|
7
|
+
path: string; // Absolute path
|
|
8
|
+
relativePath: string; // Path relative to public/ (e.g., 'images/logo.png')
|
|
9
|
+
used: boolean;
|
|
10
|
+
references: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PublicScanResult {
|
|
14
|
+
total: number;
|
|
15
|
+
used: number;
|
|
16
|
+
unused: number;
|
|
17
|
+
assets: PublicAsset[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scan for unused assets in public directory
|
|
22
|
+
*/
|
|
23
|
+
export async function scanPublicAssets(config: Config): Promise<PublicScanResult> {
|
|
24
|
+
const cwd = config.dir;
|
|
25
|
+
const publicDir = join(cwd, 'public');
|
|
26
|
+
|
|
27
|
+
if (!existsSync(publicDir)) {
|
|
28
|
+
return { total: 0, used: 0, unused: 0, assets: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 1. Find all files in public directory
|
|
32
|
+
const assetFiles = await fg('**/*', {
|
|
33
|
+
cwd: publicDir,
|
|
34
|
+
ignore: config.ignore.folders || [],
|
|
35
|
+
onlyFiles: true,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (assetFiles.length === 0) {
|
|
39
|
+
return { total: 0, used: 0, unused: 0, assets: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. Build asset map
|
|
43
|
+
const assets: PublicAsset[] = assetFiles.map((file) => ({
|
|
44
|
+
path: join(publicDir, file),
|
|
45
|
+
relativePath: '/' + file, // e.g., /images/logo.png
|
|
46
|
+
used: false,
|
|
47
|
+
references: [],
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
// 3. Find all source files to scan
|
|
51
|
+
const extGlob = `**/*{${config.extensions.join(',')}}`;
|
|
52
|
+
const sourceFiles = await fg(extGlob, {
|
|
53
|
+
cwd,
|
|
54
|
+
ignore: [...config.ignore.folders, ...config.ignore.files, 'public/**'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 4. Scan source files for references
|
|
58
|
+
for (const file of sourceFiles) {
|
|
59
|
+
const filePath = join(cwd, file);
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
62
|
+
|
|
63
|
+
for (const asset of assets) {
|
|
64
|
+
if (asset.used) continue; // Optimization: stop checking if already found (unless we want all refs)
|
|
65
|
+
|
|
66
|
+
// Check for exact path match (e.g. "/images/logo.png")
|
|
67
|
+
// We match strict usage to avoid false positives
|
|
68
|
+
if (content.includes(asset.relativePath)) {
|
|
69
|
+
asset.used = true;
|
|
70
|
+
asset.references.push(file);
|
|
71
|
+
} else {
|
|
72
|
+
// Also check for filename only usage if it's unique enough?
|
|
73
|
+
// For now, sticking to relative path for safety to avoid false positives.
|
|
74
|
+
// Maybe simple version: check if just filename exists?
|
|
75
|
+
// Common pattern: src="/images/logo.png"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Skip unreadable files
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
total: assets.length,
|
|
85
|
+
used: assets.filter((a) => a.used).length,
|
|
86
|
+
unused: assets.filter((a) => !a.used).length,
|
|
87
|
+
assets,
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface Config {
|
|
|
8
8
|
dir: string;
|
|
9
9
|
ignore: IgnoreConfig;
|
|
10
10
|
extensions: string[];
|
|
11
|
+
excludePublic?: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface ApiRoute {
|
|
@@ -26,6 +27,17 @@ export interface ScanResult {
|
|
|
26
27
|
used: number;
|
|
27
28
|
unused: number;
|
|
28
29
|
routes: ApiRoute[];
|
|
30
|
+
publicAssets?: {
|
|
31
|
+
total: number;
|
|
32
|
+
used: number;
|
|
33
|
+
unused: number;
|
|
34
|
+
assets: {
|
|
35
|
+
path: string;
|
|
36
|
+
relativePath: string;
|
|
37
|
+
used: boolean;
|
|
38
|
+
references: string[];
|
|
39
|
+
}[];
|
|
40
|
+
};
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export interface VercelConfig {
|