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 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 API routes that are not referenced in the codebase. It supports auto-detection of usage via `fetch`, `axios`, and `useSWR`, and respects `vercel.json` cron jobs.
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 API routes that are not referenced in the codebase. It supports auto-detection of usage via `fetch`, `axios`, and `useSWR`, and respects `vercel.json` cron jobs.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruny",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Find and remove unused Next.js API routes",
5
5
  "type": "module",
6
6
  "bin": {
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: fileConfig.ignore?.routes || DEFAULT_CONFIG.ignore.routes,
42
- folders: fileConfig.ignore?.folders || DEFAULT_CONFIG.ignore.folders,
43
- files: fileConfig.ignore?.files || DEFAULT_CONFIG.ignore.files,
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
- if (result.total === 0) {
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
- return;
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
- // List unused routes
69
- console.log(chalk.red.bold('āŒ Unused routes:\n'));
70
- for (const route of unused) {
71
- console.log(chalk.red(` ${route.path}`));
72
- console.log(chalk.dim(` → ${route.filePath}`));
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
- console.log(chalk.yellow.bold('šŸ—‘ļø Deleting unused routes...\n'));
101
-
102
- for (const route of unused) {
103
- const routeDir = dirname(join(config.dir, route.filePath));
104
- try {
105
- rmSync(routeDir, { recursive: true, force: true });
106
- console.log(chalk.red(` Deleted: ${route.filePath}`));
107
- } catch (err) {
108
- console.log(
109
- chalk.yellow(` Failed to delete: ${route.filePath}`)
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
- console.log(
115
- chalk.green(`\nāœ… Deleted ${unused.length} unused route(s).\n`)
116
- );
117
- } else {
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 {