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 +4 -4
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/interactive.js +58 -15
- package/src/locator.js +116 -7
- package/src/reporter.js +1 -1
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.
|
|
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.
|
|
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
package/src/cli.js
CHANGED
package/src/interactive.js
CHANGED
|
@@ -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.
|
|
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(
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(`
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
console.log(pc.
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
|