depshred 1.0.0 → 1.0.1

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
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="assets/depshred.webp" width="200" alt="depshred logo">
2
+ <img src="https://raw.githubusercontent.com/gozajpeg/depshred/master/assets/depshred.webp" width="200" alt="depshred logo">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">depshred</h1>
@@ -11,7 +11,7 @@
11
11
  <p align="center">
12
12
  <img src="https://img.shields.io/badge/run_with-npx_depshred-111111?style=flat-square" alt="Run with npx">
13
13
  <img src="https://img.shields.io/badge/node.js-18%2B_%7C_20%2B_%7C_22%2B-111111?style=flat-square" alt="Node Support">
14
- <img src="https://img.shields.io/badge/version-1.0.0-111111?style=flat-square" alt="version 1.0.0">
14
+ <img src="https://img.shields.io/badge/version-1.0.1-111111?style=flat-square" alt="version 1.0.0">
15
15
  <img src="https://img.shields.io/badge/license-MIT-111111?style=flat-square" alt="MIT license">
16
16
  </p>
17
17
 
@@ -53,14 +53,14 @@ bunx depshred
53
53
  Here is what you see when you run it in your terminal:
54
54
 
55
55
  <p align="center">
56
- <img src="assets/view01.webp" width="100%" alt="depshred terminal interface">
56
+ <img src="https://raw.githubusercontent.com/gozajpeg/depshred/master/assets/view01.webp" width="100%" alt="depshred terminal interface">
57
57
  </p>
58
58
 
59
59
  You get an easy list you can move through with your arrow keys:
60
60
 
61
61
  ```text
62
62
  +--------------------------------------------------------------------------+
63
- | depshred v1.0.0 (Node v20.12.0) |
63
+ | depshred v1.0.1 (Node v20.12.0) |
64
64
  | Target: ./my-project |
65
65
  +--------------------------------------------------------------------------+
66
66
  | Total Scanned: 24 dependencies |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "depshred",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Shred dependency bloat. Embrace modern Node.",
5
5
  "type": "module",
6
6
  "main": "./src/cli.js",
package/src/cli.js CHANGED
@@ -77,7 +77,7 @@ export async function run(rawArgs = []) {
77
77
  }
78
78
 
79
79
  if (values.version) {
80
- console.log('depshred v1.0.0');
80
+ console.log('depshred v1.0.1');
81
81
  return;
82
82
  }
83
83
 
@@ -29,7 +29,7 @@ export async function runInteractive(auditData) {
29
29
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
30
30
 
31
31
  console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
32
- console.log(pc.bold(pc.white(`| depshred v1.0.0 (Target Node: ${targetNode})`)));
32
+ console.log(pc.bold(pc.white(`| depshred v1.0.1 (Target Node: ${targetNode})`)));
33
33
  console.log(pc.bold(pc.white(`| Project: ${projectName} (${totalDependencies} dependencies scanned)`)));
34
34
  console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
35
35
  console.log(pc.red(`| [CRITICAL] ${summary.critical} native replacements (saves ~${formatBytes(totalSaveableBytes)})`));
@@ -41,27 +41,70 @@ export async function runInteractive(auditData) {
41
41
 
42
42
  if (inspecting) {
43
43
  const item = results[selectedIndex];
44
- console.log(pc.bold(`=== INSPECTING: ${item.name} (${item.tier}) ===`));
45
- console.log(`Details: ${item.details}`);
46
- console.log(`Unpacked Size: ${formatBytes(item.size)}`);
47
- console.log(`Sub-dependencies: ${item.dependenciesCount}`);
44
+ console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
45
+ console.log(pc.bold(pc.white(`| INSPECTING: ${item.name} (${item.tier})`)));
46
+ console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
47
+ console.log(`Summary: ${item.details}`);
48
+ console.log(`Unpacked Size: ${formatBytes(item.size)} | Sub-dependencies: ${item.dependenciesCount}`);
49
+ console.log(`Type: ${item.isDev ? 'Development dependency (devDependencies)' : 'Production dependency (dependencies)'}`);
48
50
  console.log('');
49
51
 
52
+ // Critical items: show Before/After diff and uninstall command
53
+ if (item.tier === 'CRITICAL') {
54
+ if (item.diff) {
55
+ console.log(pc.bold('Replacement Solution (Modern Node.js Built-in):'));
56
+ console.log(pc.green(item.diff));
57
+ console.log('');
58
+ }
59
+ console.log(pc.bold('Action:'));
60
+ console.log(pc.cyan(` npm uninstall ${item.name}`));
61
+ console.log(pc.dim(` Saves ~${formatBytes(item.size)} unpacked disk space.`));
62
+ console.log('');
63
+ }
50
64
 
65
+ // Unused items: show clear uninstall command and ignore instructions
66
+ if (item.tier === 'UNUSED') {
67
+ console.log(pc.bold('Recommended Action:'));
68
+ console.log(pc.cyan(` npm uninstall ${item.name}`));
69
+ console.log(pc.dim(` Zero imports or config references found in project.`));
70
+ console.log(pc.dim(` To permanently ignore, add to package.json: "depshred": { "ignore": ["${item.name}"] }`));
71
+ console.log('');
72
+ }
51
73
 
52
- if (item.imports && item.imports.length > 0) {
53
- console.log(pc.bold(`Imports found (${item.imports.length}):`));
54
- for (const imp of item.imports.slice(0, 5)) {
55
- console.log(pc.dim(` - ${imp.file}:${imp.line} -> ${imp.snippet}`));
74
+ // Warning items: show lighter alternatives or dormancy details
75
+ if (item.tier === 'WARN') {
76
+ if (item.altRule) {
77
+ console.log(pc.bold('Recommended Lighter Alternative:'));
78
+ console.log(` Suggested: ${pc.cyan(item.altRule.lighter)}`);
79
+ console.log(` Savings: ${pc.green(item.altRule.savings)}`);
80
+ console.log(` Details: ${item.altRule.description}`);
81
+ console.log('');
82
+ } else if (item.size > 5 * 1024 * 1024) {
83
+ console.log(pc.bold('Size Optimization Note:'));
84
+ console.log(pc.yellow(` Heavy package (${formatBytes(item.size)}). Check if dynamic import() or tree-shaking can reduce bundle weight.`));
85
+ console.log('');
56
86
  }
57
- if (item.imports.length > 5) {
58
- console.log(pc.dim(` ... and ${item.imports.length - 5} more`));
87
+
88
+ if (item.yearsDormant && item.yearsDormant >= 3) {
89
+ console.log(pc.bold('Maintenance Assessment:'));
90
+ if (item.selfContained) {
91
+ console.log(pc.dim(` Last update >${item.yearsDormant} years ago, but 0 sub-dependencies (self-contained and stable).`));
92
+ } else {
93
+ console.log(pc.yellow(` Last update >${item.yearsDormant} years ago with ${item.dependenciesCount} sub-dependencies.`));
94
+ }
95
+ console.log('');
59
96
  }
60
- console.log('');
61
97
  }
62
98
 
63
- if (item.tier === 'UNUSED') {
64
- console.log(pc.cyan(`Run: npm uninstall ${item.name}`));
99
+ // Show where imports or references were found
100
+ if (item.imports && item.imports.length > 0) {
101
+ console.log(pc.bold(`References in project (${item.imports.length}):`));
102
+ for (const imp of item.imports.slice(0, 8)) {
103
+ console.log(pc.dim(` - ${imp.file}:${imp.line} -> ${imp.snippet}`));
104
+ }
105
+ if (item.imports.length > 8) {
106
+ console.log(pc.dim(` ... and ${item.imports.length - 8} more references`));
107
+ }
65
108
  console.log('');
66
109
  }
67
110
 
@@ -136,4 +179,4 @@ export async function runInteractive(auditData) {
136
179
  process.stdin.resume();
137
180
  render();
138
181
  });
139
- }
182
+ }
package/src/locator.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, stat } from 'node:fs/promises';
2
- import { join, relative, extname } from 'node:path';
2
+ import { join, relative, extname, basename } from 'node:path';
3
3
 
4
- const SUPPORTED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx']);
4
+ const SUPPORTED_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.vue', '.svelte']);
5
5
  const IGNORED_DIRECTORIES = new Set([
6
6
  'node_modules',
7
7
  '.git',
@@ -17,6 +17,25 @@ const IGNORED_DIRECTORIES = new Set([
17
17
  '.windsurf'
18
18
  ]);
19
19
 
20
+ const CONFIG_FILE_PATTERNS = [
21
+ /^\.babelrc(\.json|\.js|\.cjs|\.mjs)?$/,
22
+ /^babel\.config(\.json|\.js|\.cjs|\.mjs)?$/,
23
+ /^\.eslintrc(\.json|\.js|\.cjs|\.yaml|\.yml)?$/,
24
+ /^eslint\.config(\.js|\.mjs|\.cjs)?$/,
25
+ /^\.prettierrc(\.json|\.js|\.cjs|\.yaml|\.yml)?$/,
26
+ /^prettier\.config(\.js|\.mjs|\.cjs)?$/,
27
+ /^tsconfig(\..+)?\.json$/,
28
+ /^jsconfig(\..+)?\.json$/,
29
+ /^postcss\.config(\.js|\.cjs|\.mjs|\.json)?$/,
30
+ /^tailwind\.config(\.js|\.cjs|\.mjs|\.ts)?$/,
31
+ /^vite\.config(\.js|\.ts|\.mjs)?$/,
32
+ /^webpack\.config(\.js|\.cjs|\.mjs|\.ts)?$/,
33
+ /^rollup\.config(\.js|\.mjs)?$/,
34
+ /^next\.config(\.js|\.mjs|\.ts)?$/,
35
+ /^jest\.config(\.js|\.ts|\.json)?$/,
36
+ /^vitest\.config(\.js|\.ts)?$/
37
+ ];
38
+
20
39
  /**
21
40
  * Normalizes an import specifier to its root npm package name.
22
41
  * e.g. "lodash/map" -> "lodash", "@scoped/pkg/deep" -> "@scoped/pkg"
@@ -80,10 +99,34 @@ async function getSourceFiles(dir) {
80
99
  }
81
100
 
82
101
  /**
83
- * Scan project source files to locate package imports and detect unused dependencies.
102
+ * Find root-level config files in a directory.
103
+ */
104
+ async function getRootConfigFiles(dir) {
105
+ const configFiles = [];
106
+ let entries;
107
+ try {
108
+ entries = await readdir(dir, { withFileTypes: true });
109
+ } catch {
110
+ return configFiles;
111
+ }
112
+
113
+ for (const entry of entries) {
114
+ if (entry.isFile()) {
115
+ const name = entry.name.toLowerCase();
116
+ if (CONFIG_FILE_PATTERNS.some((pat) => pat.test(name))) {
117
+ configFiles.push(entry.name);
118
+ }
119
+ }
120
+ }
121
+ return configFiles;
122
+ }
123
+
124
+ /**
125
+ * Scan project source files and config files to locate package imports and detect unused dependencies.
84
126
  *
85
127
  * @param {string} rootDir - Root directory of the project
86
128
  * @param {string[]} declaredDependencies - List of package names from package.json
129
+ * @param {object} scripts - package.json scripts object
87
130
  * @returns {Promise<{ importsMap: Map<string, Array<{file: string, line: number, snippet: string}>>, unusedPackages: string[], scannedFilesCount: number }>}
88
131
  */
89
132
  export async function locateImports(rootDir, declaredDependencies = [], scripts = {}) {
@@ -91,7 +134,73 @@ export async function locateImports(rootDir, declaredDependencies = [], scripts
91
134
  const importsMap = new Map();
92
135
  const importedSet = new Set();
93
136
 
94
- // Scan package.json scripts for CLI tool references (e.g. nodemon, rimraf)
137
+ // Rule 1: Always ignore depshred itself (never flag depshred as unused within a project)
138
+ if (declaredDependencies.includes('depshred')) {
139
+ importedSet.add('depshred');
140
+ importsMap.set('depshred', [{
141
+ file: 'package.json',
142
+ line: 1,
143
+ snippet: 'depshred CLI dependency auditor'
144
+ }]);
145
+ }
146
+
147
+ // Rule 2: Ambient TypeScript definitions (@types/*) are used by compiler, never imported in source code
148
+ for (const pkg of declaredDependencies) {
149
+ if (pkg.startsWith('@types/')) {
150
+ importedSet.add(pkg);
151
+ if (!importsMap.has(pkg)) importsMap.set(pkg, []);
152
+ importsMap.get(pkg).push({
153
+ file: 'tsconfig.json',
154
+ line: 1,
155
+ snippet: 'TypeScript ambient type definition'
156
+ });
157
+ }
158
+ }
159
+
160
+ // Rule 3: Scan root configuration files for package names and plugin/preset references
161
+ const rootConfigFiles = await getRootConfigFiles(rootDir);
162
+ for (const configName of rootConfigFiles) {
163
+ let content;
164
+ try {
165
+ content = await readFile(join(rootDir, configName), 'utf8');
166
+ } catch {
167
+ continue;
168
+ }
169
+
170
+ for (const pkg of declaredDependencies) {
171
+ if (importedSet.has(pkg)) continue;
172
+
173
+ // Exact package match (quoted or word boundary)
174
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
175
+ const directRegex = new RegExp(`['"]${escaped}['"]|\\b${escaped}\\b`);
176
+
177
+ // Shorthand matches for common config tools (e.g. "react" for "eslint-plugin-react" or "react-app" for "eslint-config-react-app")
178
+ let shorthandMatch = false;
179
+ if (configName.includes('eslint')) {
180
+ const eslintPlugin = pkg.replace(/^eslint-plugin-/, '');
181
+ const eslintConfig = pkg.replace(/^eslint-config-/, '');
182
+ if (eslintPlugin !== pkg && new RegExp(`['"]${eslintPlugin}['"]`).test(content)) shorthandMatch = true;
183
+ if (eslintConfig !== pkg && new RegExp(`['"]${eslintConfig}['"]`).test(content)) shorthandMatch = true;
184
+ } else if (configName.includes('babel')) {
185
+ const babelPreset = pkg.replace(/^(@babel\/preset-|babel-preset-)/, '');
186
+ const babelPlugin = pkg.replace(/^(@babel\/plugin-|babel-plugin-)/, '');
187
+ if (babelPreset !== pkg && new RegExp(`['"]${babelPreset}['"]`).test(content)) shorthandMatch = true;
188
+ if (babelPlugin !== pkg && new RegExp(`['"]${babelPlugin}['"]`).test(content)) shorthandMatch = true;
189
+ }
190
+
191
+ if (directRegex.test(content) || shorthandMatch) {
192
+ importedSet.add(pkg);
193
+ if (!importsMap.has(pkg)) importsMap.set(pkg, []);
194
+ importsMap.get(pkg).push({
195
+ file: configName,
196
+ line: 1,
197
+ snippet: `Config reference in ${configName}`
198
+ });
199
+ }
200
+ }
201
+ }
202
+
203
+ // Rule 4: Scan package.json scripts for CLI tool references (e.g. nodemon, rimraf, tsc, prettier)
95
204
  for (const [scriptName, scriptCmd] of Object.entries(scripts)) {
96
205
  if (typeof scriptCmd !== 'string') continue;
97
206
  for (const pkg of declaredDependencies) {
@@ -111,6 +220,7 @@ export async function locateImports(rootDir, declaredDependencies = [], scripts
111
220
  }
112
221
  }
113
222
 
223
+ // Rule 5: Scan project source files for import / require / export from
114
224
  for (const filePath of sourceFiles) {
115
225
  let content;
116
226
  try {
@@ -124,7 +234,6 @@ export async function locateImports(rootDir, declaredDependencies = [], scripts
124
234
 
125
235
  for (let i = 0; i < lines.length; i++) {
126
236
  const line = lines[i];
127
- // Skip empty or comment-only lines
128
237
  const trimmed = line.trim();
129
238
  if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) {
130
239
  continue;
@@ -152,7 +261,7 @@ export async function locateImports(rootDir, declaredDependencies = [], scripts
152
261
  }
153
262
  }
154
263
 
155
- // Detect unused packages: declared in package.json but never imported in source code
264
+ // Detect unused packages: declared in package.json but never referenced anywhere
156
265
  const unusedPackages = declaredDependencies.filter((pkg) => !importedSet.has(pkg));
157
266
 
158
267
  return {
@@ -160,4 +269,4 @@ export async function locateImports(rootDir, declaredDependencies = [], scripts
160
269
  unusedPackages,
161
270
  scannedFilesCount: sourceFiles.length
162
271
  };
163
- }
272
+ }
package/src/reporter.js CHANGED
@@ -9,7 +9,7 @@ export function printScorecard(auditData, options = {}) {
9
9
 
10
10
  console.log('');
11
11
  console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
12
- console.log(pc.bold(pc.white(`| depshred v1.0.0 (Target Node: ${targetNode})`)));
12
+ console.log(pc.bold(pc.white(`| depshred v1.0.1 (Target Node: ${targetNode})`)));
13
13
  console.log(pc.bold(pc.white(`| Project: ${projectName} (${totalDependencies} dependencies scanned)`)));
14
14
  console.log(pc.bold(pc.white('+--------------------------------------------------------------------------+')));
15
15